Various minor fixes. WARNING: Adding of books is currently broken.

This commit is contained in:
Kovid Goyal 2009-05-14 11:26:37 -07:00
parent 2e0ad5d1e0
commit 1a117fd070
26 changed files with 669 additions and 657 deletions

View File

@ -9,43 +9,43 @@ a backend that implement the Device interface for the SONY PRS500 Reader.
from calibre.customize import Plugin from calibre.customize import Plugin
class DevicePlugin(Plugin): class DevicePlugin(Plugin):
""" """
Defines the interface that should be implemented by backends that Defines the interface that should be implemented by backends that
communicate with an ebook reader. communicate with an ebook reader.
The C{end_session} variables are used for USB session management. Sometimes The C{end_session} variables are used for USB session management. Sometimes
the front-end needs to call several methods one after another, in which case the front-end needs to call several methods one after another, in which case
the USB session should not be closed after each method call. the USB session should not be closed after each method call.
""" """
type = _('Device Interface') type = _('Device Interface')
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"] FORMATS = ["lrf", "rtf", "pdf", "txt"]
VENDOR_ID = 0x0000 VENDOR_ID = 0x0000
PRODUCT_ID = 0x0000 PRODUCT_ID = 0x0000
# BCD can be either None to not distinguish between devices based on BCD, or # BCD can be either None to not distinguish between devices based on BCD, or
# it can be a list of the BCD numbers of all devices supported by this driver. # it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None BCD = None
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
# Whether the metadata on books can be set via the GUI. # Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = True CAN_SET_METADATA = True
def reset(self, key='-1', log_packets=False, report_progress=None) : def reset(self, key='-1', log_packets=False, report_progress=None) :
""" """
@param key: The key to unlock the device @param key: The key to unlock the device
@param log_packets: If true the packet stream to/from the device is logged @param log_packets: If true the packet stream to/from the device is logged
@param report_progress: Function that is called with a % progress @param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
""" """
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def get_fdi(cls): def get_fdi(cls):
'''Return the FDI description of this device for HAL on linux.''' '''Return the FDI description of this device for HAL on linux.'''
return '' return ''
@classmethod @classmethod
def can_handle(cls, device_info): def can_handle(cls, device_info):
''' '''
@ -54,40 +54,40 @@ class DevicePlugin(Plugin):
is only called after the vendor, product ids and the bcd have matched, so is only called after the vendor, product ids and the bcd have matched, so
it can do some relatively time intensive checks. The default implementation it can do some relatively time intensive checks. The default implementation
returns True. returns True.
:param device_info: On windows a device ID string. On Unix a tuple of :param device_info: On windows a device ID string. On Unix a tuple of
``(vendor_id, product_id, bcd)``. ``(vendor_id, product_id, bcd)``.
''' '''
return True return True
def open(self): def open(self):
''' '''
Perform any device specific initialization. Called after the device is Perform any device specific initialization. Called after the device is
detected but before any other functions that communicate with the device. detected but before any other functions that communicate with the device.
For example: For devices that present themselves as USB Mass storage For example: For devices that present themselves as USB Mass storage
devices, this method would be responsible for mounting the device or devices, this method would be responsible for mounting the device or
if the device has been automounted, for finding out where it has been if the device has been automounted, for finding out where it has been
mounted. The driver for the PRS505 has a implementation of this function mounted. The driver for the PRS505 has a implementation of this function
that should serve as a good example for USB Mass storage devices. that should serve as a good example for USB Mass storage devices.
''' '''
raise NotImplementedError() raise NotImplementedError()
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
''' '''
@param report_progress: Function that is called with a % progress @param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
''' '''
raise NotImplementedError() raise NotImplementedError()
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
""" """
Ask device for device information. See L{DeviceInfoQuery}. Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type) @return: (device name, device version, software version on device, mime type)
""" """
raise NotImplementedError() raise NotImplementedError()
def card_prefix(self, end_session=True): def card_prefix(self, end_session=True):
''' '''
Return a 2 element list of the prefix to paths on the cards. Return a 2 element list of the prefix to paths on the cards.
@ -99,9 +99,9 @@ class DevicePlugin(Plugin):
(None, None) (None, None)
''' '''
raise NotImplementedError() raise NotImplementedError()
def total_space(self, end_session=True): def total_space(self, end_session=True):
""" """
Get total space available on the mountpoints: Get total space available on the mountpoints:
1. Main memory 1. Main memory
2. Memory Card A 2. Memory Card A
@ -111,9 +111,9 @@ class DevicePlugin(Plugin):
particular device doesn't have any of these locations it should return 0. particular device doesn't have any of these locations it should return 0.
""" """
raise NotImplementedError() raise NotImplementedError()
def free_space(self, end_session=True): def free_space(self, end_session=True):
""" """
Get free space available on the mountpoints: Get free space available on the mountpoints:
1. Main memory 1. Main memory
2. Card A 2. Card A
@ -121,20 +121,20 @@ class DevicePlugin(Plugin):
@return: A 3 element list with free space in bytes of (1, 2, 3). If a @return: A 3 element list with free space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return -1. particular device doesn't have any of these locations it should return -1.
""" """
raise NotImplementedError() raise NotImplementedError()
def books(self, oncard=None, end_session=True): def books(self, oncard=None, end_session=True):
""" """
Return a list of ebooks on the device. Return a list of ebooks on the device.
@param oncard: If 'carda' or 'cardb' return a list of ebooks on the @param oncard: If 'carda' or 'cardb' return a list of ebooks on the
specific storage card, otherwise return list of ebooks specific storage card, otherwise return list of ebooks
in main memory of device. If a card is specified and no in main memory of device. If a card is specified and no
books are on the card return empty list. books are on the card return empty list.
@return: A BookList. @return: A BookList.
""" """
raise NotImplementedError() raise NotImplementedError()
def upload_books(self, files, names, on_card=None, end_session=True, def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None): metadata=None):
''' '''
@ -144,26 +144,26 @@ class DevicePlugin(Plugin):
free space on the device. The text of the FreeSpaceError must contain the free space on the device. The text of the FreeSpaceError must contain the
word "card" if C{on_card} is not None otherwise it must contain the word "memory". word "card" if C{on_card} is not None otherwise it must contain the word "memory".
@param files: A list of paths and/or file-like objects. @param files: A list of paths and/or file-like objects.
@param names: A list of file names that the books should have @param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files) once uploaded to the device. len(names) == len(files)
@return: A list of 3-element tuples. The list is meant to be passed @return: A list of 3-element tuples. The list is meant to be passed
to L{add_books_to_metadata}. to L{add_books_to_metadata}.
@param metadata: If not None, it is a list of dictionaries. Each dictionary @param metadata: If not None, it is a list of dictionaries. Each dictionary
will have at least the key tags to allow the driver to choose book location will have at least the key tags to allow the driver to choose book location
based on tags. len(metadata) == len(files). If your device does not support based on tags. len(metadata) == len(files). If your device does not support
hierarchical ebook folders, you can safely ignore this parameter. hierarchical ebook folders, you can safely ignore this parameter.
''' '''
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def add_books_to_metadata(cls, locations, metadata, booklists): def add_books_to_metadata(cls, locations, metadata, booklists):
''' '''
Add locations to the booklists. This function must not communicate with Add locations to the booklists. This function must not communicate with
the device. the device.
@param locations: Result of a call to L{upload_books} @param locations: Result of a call to L{upload_books}
@param metadata: List of dictionaries. Each dictionary must have the @param metadata: List of dictionaries. Each dictionary must have the
keys C{title}, C{authors}, C{author_sort}, C{cover}, C{tags}. keys C{title}, C{authors}, C{author_sort}, C{cover}, C{tags}.
The value of the C{cover} The value of the C{cover}
element can be None or a three element tuple (width, height, data) element can be None or a three element tuple (width, height, data)
where data is the image data in JPEG format as a string. C{tags} must be where data is the image data in JPEG format as a string. C{tags} must be
a possibly empty list of strings. C{authors} must be a string. a possibly empty list of strings. C{authors} must be a string.
@ -172,22 +172,22 @@ class DevicePlugin(Plugin):
The dictionary can also have an optional key "tag order" which should be The dictionary can also have an optional key "tag order" which should be
another dictionary that maps tag names to lists of book ids. The ids are another dictionary that maps tag names to lists of book ids. The ids are
ids from the book database. ids from the book database.
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
(L{books}(oncard=None), L{books}(oncard='carda'), (L{books}(oncard=None), L{books}(oncard='carda'),
L{books}(oncard='cardb')). L{books}(oncard='cardb')).
''' '''
raise NotImplementedError raise NotImplementedError
def delete_books(self, paths, end_session=True): def delete_books(self, paths, end_session=True):
''' '''
Delete books at paths on device. Delete books at paths on device.
''' '''
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def remove_books_from_metadata(cls, paths, booklists): def remove_books_from_metadata(cls, paths, booklists):
''' '''
Remove books from the metadata list. This function must not communicate Remove books from the metadata list. This function must not communicate
with the device. with the device.
@param paths: paths to books on the device. @param paths: paths to books on the device.
@param booklists: A tuple containing the result of calls to @param booklists: A tuple containing the result of calls to
@ -195,7 +195,7 @@ class DevicePlugin(Plugin):
L{books}(oncard='cardb')). L{books}(oncard='cardb')).
''' '''
raise NotImplementedError() raise NotImplementedError()
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
''' '''
Update metadata on device. Update metadata on device.
@ -204,8 +204,8 @@ class DevicePlugin(Plugin):
L{books}(oncard='cardb')). L{books}(oncard='cardb')).
''' '''
raise NotImplementedError() raise NotImplementedError()
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
''' '''
Read the file at C{path} on the device and write it to outfile. Read the file at C{path} on the device and write it to outfile.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call @param outfile: file object like C{sys.stdout} or the result of an C{open} call
@ -231,13 +231,13 @@ class DevicePlugin(Plugin):
def settings(cls): def settings(cls):
''' '''
Should return an opts object. The opts object should have one attribute Should return an opts object. The opts object should have one attribute
`formats` whihc is an ordered list of formats for the device. `format_map` which is an ordered list of formats for the device.
''' '''
raise NotImplementedError() raise NotImplementedError()
class BookList(list): class BookList(list):
''' '''
A list of books. Each Book object must have the fields: A list of books. Each Book object must have the fields:
@ -247,21 +247,21 @@ class BookList(list):
4. datetime (a UTC time tuple) 4. datetime (a UTC time tuple)
5. path (path on the device to the book) 5. path (path on the device to the book)
6. thumbnail (can be None) 6. thumbnail (can be None)
7. tags (a list of strings, can be empty). 7. tags (a list of strings, can be empty).
''' '''
__getslice__ = None __getslice__ = None
__setslice__ = None __setslice__ = None
def supports_tags(self): def supports_tags(self):
''' Return True if the the device supports tags (collections) for this book list. ''' ''' Return True if the the device supports tags (collections) for this book list. '''
raise NotImplementedError() raise NotImplementedError()
def set_tags(self, book, tags): def set_tags(self, book, tags):
''' '''
Set the tags for C{book} to C{tags}. Set the tags for C{book} to C{tags}.
@param tags: A list of strings. Can be empty. @param tags: A list of strings. Can be empty.
@param book: A book object that is in this BookList. @param book: A book object that is in this BookList.
''' '''
raise NotImplementedError() raise NotImplementedError()

View File

@ -53,7 +53,7 @@ class KINDLE(USBMS):
@classmethod @classmethod
def metadata_from_path(cls, path): def metadata_from_path(cls, path):
from calibre.devices.usbms.driver import metadata_from_formats from calibre.ebooks.metadata.meta import metadata_from_formats
mi = metadata_from_formats([path]) mi = metadata_from_formats([path])
if mi.title == _('Unknown') or ('-asin' in mi.title and '-type' in mi.title): if mi.title == _('Unknown') or ('-asin' in mi.title and '-type' in mi.title):
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path)) match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))

View File

@ -1,8 +1,8 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" """
This module contains the logic for dealing with XML book lists found This module contains the logic for dealing with XML book lists found
in the reader cache. in the reader cache.
""" """
import xml.dom.minidom as dom import xml.dom.minidom as dom
from base64 import b64decode as decode from base64 import b64decode as decode
@ -25,16 +25,16 @@ def sortable_title(title):
class book_metadata_field(object): class book_metadata_field(object):
""" Represents metadata stored as an attribute """ """ Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None): def __init__(self, attr, formatter=None, setter=None):
self.attr = attr self.attr = attr
self.formatter = formatter self.formatter = formatter
self.setter = setter self.setter = setter
def __get__(self, obj, typ=None): def __get__(self, obj, typ=None):
""" Return a string. String may be empty if self.attr is absent """ """ Return a string. String may be empty if self.attr is absent """
return self.formatter(obj.elem.getAttribute(self.attr)) if \ return self.formatter(obj.elem.getAttribute(self.attr)) if \
self.formatter else obj.elem.getAttribute(self.attr).strip() self.formatter else obj.elem.getAttribute(self.attr).strip()
def __set__(self, obj, val): def __set__(self, obj, val):
""" Set the attribute """ """ Set the attribute """
val = self.setter(val) if self.setter else val val = self.setter(val) if self.setter else val
@ -44,7 +44,7 @@ class book_metadata_field(object):
class Book(object): class Book(object):
""" Provides a view onto the XML element that represents a book """ """ Provides a view onto the XML element that represents a book """
title = book_metadata_field("title") title = book_metadata_field("title")
authors = book_metadata_field("author", \ authors = book_metadata_field("author", \
formatter=lambda x: x if x and x.strip() else "Unknown") formatter=lambda x: x if x and x.strip() else "Unknown")
@ -66,12 +66,12 @@ class Book(object):
def fset(self, val): def fset(self, val):
self.elem.setAttribute('titleSorter', sortable_title(unicode(val))) self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
return property(doc=doc, fget=fget, fset=fset) return property(doc=doc, fget=fget, fset=fset)
@dynamic_property @dynamic_property
def thumbnail(self): def thumbnail(self):
doc = \ doc = \
""" """
The thumbnail. Should be a height 68 image. The thumbnail. Should be a height 68 image.
Setting is not supported. Setting is not supported.
""" """
def fget(self): def fget(self):
@ -83,18 +83,18 @@ class Book(object):
break break
rc = "" rc = ""
for node in th.childNodes: for node in th.childNodes:
if node.nodeType == node.TEXT_NODE: if node.nodeType == node.TEXT_NODE:
rc += node.data rc += node.data
return decode(rc) return decode(rc)
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
@dynamic_property @dynamic_property
def path(self): def path(self):
doc = """ Absolute path to book on device. Setting not supported. """ doc = """ Absolute path to book on device. Setting not supported. """
def fget(self): def fget(self):
return self.root + self.rpath return self.root + self.rpath
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
@dynamic_property @dynamic_property
def db_id(self): def db_id(self):
doc = '''The database id in the application database that this file corresponds to''' doc = '''The database id in the application database that this file corresponds to'''
@ -103,20 +103,20 @@ class Book(object):
if match: if match:
return int(match.group(1)) return int(match.group(1))
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
def __init__(self, node, tags=[], prefix="", root="/Data/media/"): def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
self.elem = node self.elem = node
self.prefix = prefix self.prefix = prefix
self.root = root self.root = root
self.tags = tags self.tags = tags
def __str__(self): def __str__(self):
""" Return a utf-8 encoded string with title author and path information """ """ Return a utf-8 encoded string with title author and path information """
return self.title.encode('utf-8') + " by " + \ return self.title.encode('utf-8') + " by " + \
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8') self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
def fix_ids(media, cache): def fix_ids(media, cache, *args):
''' '''
Adjust ids in cache to correspond with media. Adjust ids in cache to correspond with media.
''' '''
@ -131,16 +131,16 @@ def fix_ids(media, cache):
child.setAttribute("id", str(cid)) child.setAttribute("id", str(cid))
cid += 1 cid += 1
media.set_next_id(str(cid)) media.set_next_id(str(cid))
class BookList(_BookList): class BookList(_BookList):
""" """
A list of L{Book}s. Created from an XML file. Can write list A list of L{Book}s. Created from an XML file. Can write list
to an XML file. to an XML file.
""" """
__getslice__ = None __getslice__ = None
__setslice__ = None __setslice__ = None
def __init__(self, root="/Data/media/", sfile=None): def __init__(self, root="/Data/media/", sfile=None):
_BookList.__init__(self) _BookList.__init__(self)
self.tag_order = {} self.tag_order = {}
@ -163,25 +163,25 @@ class BookList(_BookList):
if records: if records:
self.prefix = 'xs1:' self.prefix = 'xs1:'
self.root = records[0] self.root = records[0]
self.proot = root self.proot = root
for book in self.document.getElementsByTagName(self.prefix + "text"): for book in self.document.getElementsByTagName(self.prefix + "text"):
id = book.getAttribute('id') id = book.getAttribute('id')
pl = [i.getAttribute('title') for i in self.get_playlists(id)] pl = [i.getAttribute('title') for i in self.get_playlists(id)]
self.append(Book(book, root=root, prefix=self.prefix, tags=pl)) self.append(Book(book, root=root, prefix=self.prefix, tags=pl))
def supports_tags(self): def supports_tags(self):
return bool(self.prefix) return bool(self.prefix)
def playlists(self): def playlists(self):
return self.root.getElementsByTagName(self.prefix+'playlist') return self.root.getElementsByTagName(self.prefix+'playlist')
def playlist_items(self): def playlist_items(self):
plitems = [] plitems = []
for pl in self.playlists(): for pl in self.playlists():
plitems.extend(pl.getElementsByTagName(self.prefix+'item')) plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
return plitems return plitems
def purge_corrupted_files(self): def purge_corrupted_files(self):
if not self.root: if not self.root:
return [] return []
@ -193,32 +193,32 @@ class BookList(_BookList):
c.parentNode.removeChild(c) c.parentNode.removeChild(c)
c.unlink() c.unlink()
return paths return paths
def purge_empty_playlists(self): def purge_empty_playlists(self):
''' Remove all playlist entries that have no children. ''' ''' Remove all playlist entries that have no children. '''
for pl in self.playlists(): for pl in self.playlists():
if not pl.getElementsByTagName(self.prefix + 'item'): if not pl.getElementsByTagName(self.prefix + 'item'):
pl.parentNode.removeChild(pl) pl.parentNode.removeChild(pl)
pl.unlink() pl.unlink()
def _delete_book(self, node): def _delete_book(self, node):
nid = node.getAttribute('id') nid = node.getAttribute('id')
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
node.unlink() node.unlink()
self.remove_from_playlists(nid) self.remove_from_playlists(nid)
def delete_book(self, cid): def delete_book(self, cid):
''' '''
Remove DOM node corresponding to book with C{id == cid}. Remove DOM node corresponding to book with C{id == cid}.
Also remove book from any collections it is part of. Also remove book from any collections it is part of.
''' '''
for book in self: for book in self:
if str(book.id) == str(cid): if str(book.id) == str(cid):
self.remove(book) self.remove(book)
self._delete_book(book.elem) self._delete_book(book.elem)
break break
def remove_book(self, path): def remove_book(self, path):
''' '''
Remove DOM node corresponding to book with C{path == path}. Remove DOM node corresponding to book with C{path == path}.
@ -227,15 +227,15 @@ class BookList(_BookList):
for book in self: for book in self:
if path.endswith(book.rpath): if path.endswith(book.rpath):
self.remove(book) self.remove(book)
self._delete_book(book.elem) self._delete_book(book.elem)
break break
def next_id(self): def next_id(self):
return self.document.documentElement.getAttribute('nextID') return self.document.documentElement.getAttribute('nextID')
def set_next_id(self, id): def set_next_id(self, id):
self.document.documentElement.setAttribute('nextID', str(id)) self.document.documentElement.setAttribute('nextID', str(id))
def max_id(self): def max_id(self):
max = 0 max = 0
for child in self.root.childNodes: for child in self.root.childNodes:
@ -243,15 +243,15 @@ class BookList(_BookList):
nid = int(child.getAttribute('id')) nid = int(child.getAttribute('id'))
if nid > max: if nid > max:
max = nid max = nid
return max return max
def book_by_path(self, path): def book_by_path(self, path):
for child in self.root.childNodes: for child in self.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"): if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
if path == child.getAttribute('path'): if path == child.getAttribute('path'):
return child return child
return None return None
def add_book(self, info, name, size, ctime): def add_book(self, info, name, size, ctime):
""" Add a node into DOM tree representing a book """ """ Add a node into DOM tree representing a book """
book = self.book_by_path(name) book = self.book_by_path(name)
@ -262,23 +262,23 @@ class BookList(_BookList):
cid = self.max_id()+1 cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1" sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = { attrs = {
"title" : info["title"], "title" : info["title"],
'titleSorter' : sortable_title(info['title']), 'titleSorter' : sortable_title(info['title']),
"author" : info["authors"] if info['authors'] else 'Unknown', \ "author" : info["authors"] if info['authors'] else 'Unknown', \
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size) "mime":mime, "path":name, "size":str(size)
} }
for attr in attrs.keys(): for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr)) node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr]) node.setAttribute(attr, attrs[attr])
try: try:
w, h, data = info["cover"] w, h, data = info["cover"]
except TypeError: except TypeError:
w, h, data = None, None, None w, h, data = None, None, None
if data: if data:
th = self.document.createElement(self.prefix + "thumbnail") th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w)) th.setAttribute("width", str(w))
th.setAttribute("height", str(h)) th.setAttribute("height", str(h))
jpeg = self.document.createElement(self.prefix + "jpeg") jpeg = self.document.createElement(self.prefix + "jpeg")
@ -294,15 +294,15 @@ class BookList(_BookList):
if info.has_key('tag order'): if info.has_key('tag order'):
self.tag_order.update(info['tag order']) self.tag_order.update(info['tag order'])
self.set_playlists(book.id, info['tags']) self.set_playlists(book.id, info['tags'])
def playlist_by_title(self, title): def playlist_by_title(self, title):
for pl in self.playlists(): for pl in self.playlists():
if pl.getAttribute('title').lower() == title.lower(): if pl.getAttribute('title').lower() == title.lower():
return pl return pl
def add_playlist(self, title): def add_playlist(self, title):
cid = self.max_id()+1 cid = self.max_id()+1
pl = self.document.createElement(self.prefix+'playlist') pl = self.document.createElement(self.prefix+'playlist')
pl.setAttribute('sourceid', '0') pl.setAttribute('sourceid', '0')
pl.setAttribute('id', str(cid)) pl.setAttribute('id', str(cid))
@ -316,18 +316,18 @@ class BookList(_BookList):
except AttributeError: except AttributeError:
continue continue
return pl return pl
def remove_from_playlists(self, id): def remove_from_playlists(self, id):
for pli in self.playlist_items(): for pli in self.playlist_items():
if pli.getAttribute('id') == str(id): if pli.getAttribute('id') == str(id):
pli.parentNode.removeChild(pli) pli.parentNode.removeChild(pli)
pli.unlink() pli.unlink()
def set_tags(self, book, tags): def set_tags(self, book, tags):
book.tags = tags book.tags = tags
self.set_playlists(book.id, tags) self.set_playlists(book.id, tags)
def set_playlists(self, id, collections): def set_playlists(self, id, collections):
self.remove_from_playlists(id) self.remove_from_playlists(id)
for collection in set(collections): for collection in set(collections):
@ -337,7 +337,7 @@ class BookList(_BookList):
item = self.document.createElement(self.prefix+'item') item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(id)) item.setAttribute('id', str(id))
coll.appendChild(item) coll.appendChild(item)
def get_playlists(self, id): def get_playlists(self, id):
ans = [] ans = []
for pl in self.playlists(): for pl in self.playlists():
@ -346,12 +346,12 @@ class BookList(_BookList):
ans.append(pl) ans.append(pl)
continue continue
return ans return ans
def book_by_id(self, id): def book_by_id(self, id):
for book in self: for book in self:
if str(book.id) == str(id): if str(book.id) == str(id):
return book return book
def reorder_playlists(self): def reorder_playlists(self):
for title in self.tag_order.keys(): for title in self.tag_order.keys():
pl = self.playlist_by_title(title) pl = self.playlist_by_title(title)
@ -364,7 +364,7 @@ class BookList(_BookList):
map[i] = j map[i] = j
pl_book_ids = [i for i in pl_book_ids if i is not None] pl_book_ids = [i for i in pl_book_ids if i is not None]
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids] ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
if len(ordered_ids) < len(pl.childNodes): if len(ordered_ids) < len(pl.childNodes):
continue continue
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')] children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
@ -374,8 +374,8 @@ class BookList(_BookList):
for id in ordered_ids: for id in ordered_ids:
item = self.document.createElement(self.prefix+'item') item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(map[id])) item.setAttribute('id', str(map[id]))
pl.appendChild(item) pl.appendChild(item)
def write(self, stream): def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """ """ Write XML representation of DOM tree to C{stream} """
stream.write(self.document.toxml('utf-8')) stream.write(self.document.toxml('utf-8'))

View File

@ -47,6 +47,7 @@ from calibre.devices.prs500.prstypes import *
from calibre.devices.errors import * from calibre.devices.errors import *
from calibre.devices.prs500.books import BookList, fix_ids from calibre.devices.prs500.books import BookList, fix_ids
from calibre import __author__, __appname__ from calibre import __author__, __appname__
from calibre.devices.usbms.deviceconfig import DeviceConfig
# Protocol versions this driver has been tested with # Protocol versions this driver has been tested with
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
@ -76,7 +77,7 @@ class File(object):
return self.name return self.name
class PRS500(DevicePlugin): class PRS500(DeviceConfig, DevicePlugin):
""" """
Implements the backend for communication with the SONY Reader. Implements the backend for communication with the SONY Reader.
@ -624,6 +625,8 @@ class PRS500(DevicePlugin):
data_type=FreeSpaceAnswer, \ data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0] command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free ) data.append( pkt.free )
data = [x for x in data if x != 0]
data.append(0)
return data return data
def _exists(self, path): def _exists(self, path):

View File

@ -15,11 +15,11 @@ from calibre.devices import strptime
strftime = functools.partial(_strftime, zone=time.gmtime) strftime = functools.partial(_strftime, zone=time.gmtime)
MIME_MAP = { MIME_MAP = {
"lrf" : "application/x-sony-bbeb", "lrf" : "application/x-sony-bbeb",
'lrx' : 'application/x-sony-bbeb', 'lrx' : 'application/x-sony-bbeb',
"rtf" : "application/rtf", "rtf" : "application/rtf",
"pdf" : "application/pdf", "pdf" : "application/pdf",
"txt" : "text/plain" , "txt" : "text/plain" ,
'epub': 'application/epub+zip', 'epub': 'application/epub+zip',
} }
@ -32,16 +32,16 @@ def sortable_title(title):
class book_metadata_field(object): class book_metadata_field(object):
""" Represents metadata stored as an attribute """ """ Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None): def __init__(self, attr, formatter=None, setter=None):
self.attr = attr self.attr = attr
self.formatter = formatter self.formatter = formatter
self.setter = setter self.setter = setter
def __get__(self, obj, typ=None): def __get__(self, obj, typ=None):
""" Return a string. String may be empty if self.attr is absent """ """ Return a string. String may be empty if self.attr is absent """
return self.formatter(obj.elem.getAttribute(self.attr)) if \ return self.formatter(obj.elem.getAttribute(self.attr)) if \
self.formatter else obj.elem.getAttribute(self.attr).strip() self.formatter else obj.elem.getAttribute(self.attr).strip()
def __set__(self, obj, val): def __set__(self, obj, val):
""" Set the attribute """ """ Set the attribute """
val = self.setter(val) if self.setter else val val = self.setter(val) if self.setter else val
@ -52,7 +52,7 @@ class book_metadata_field(object):
class Book(object): class Book(object):
""" Provides a view onto the XML element that represents a book """ """ Provides a view onto the XML element that represents a book """
title = book_metadata_field("title") title = book_metadata_field("title")
authors = book_metadata_field("author", \ authors = book_metadata_field("author", \
formatter=lambda x: x if x and x.strip() else _('Unknown')) formatter=lambda x: x if x and x.strip() else _('Unknown'))
@ -63,7 +63,7 @@ class Book(object):
size = book_metadata_field("size", formatter=lambda x : int(float(x))) size = book_metadata_field("size", formatter=lambda x : int(float(x)))
# When setting this attribute you must use an epoch # When setting this attribute you must use an epoch
datetime = book_metadata_field("date", formatter=strptime, setter=strftime) datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
@dynamic_property @dynamic_property
def title_sorter(self): def title_sorter(self):
doc = '''String to sort the title. If absent, title is returned''' doc = '''String to sort the title. If absent, title is returned'''
@ -75,12 +75,12 @@ class Book(object):
def fset(self, val): def fset(self, val):
self.elem.setAttribute('titleSorter', sortable_title(unicode(val))) self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
return property(doc=doc, fget=fget, fset=fset) return property(doc=doc, fget=fget, fset=fset)
@dynamic_property @dynamic_property
def thumbnail(self): def thumbnail(self):
doc = \ doc = \
""" """
The thumbnail. Should be a height 68 image. The thumbnail. Should be a height 68 image.
Setting is not supported. Setting is not supported.
""" """
def fget(self): def fget(self):
@ -94,18 +94,18 @@ class Book(object):
break break
rc = "" rc = ""
for node in th.childNodes: for node in th.childNodes:
if node.nodeType == node.TEXT_NODE: if node.nodeType == node.TEXT_NODE:
rc += node.data rc += node.data
return decode(rc) return decode(rc)
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
@dynamic_property @dynamic_property
def path(self): def path(self):
doc = """ Absolute path to book on device. Setting not supported. """ doc = """ Absolute path to book on device. Setting not supported. """
def fget(self): def fget(self):
return self.mountpath + self.rpath return self.mountpath + self.rpath
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
@dynamic_property @dynamic_property
def db_id(self): def db_id(self):
doc = '''The database id in the application database that this file corresponds to''' doc = '''The database id in the application database that this file corresponds to'''
@ -114,13 +114,13 @@ class Book(object):
if match: if match:
return int(match.group(1)) return int(match.group(1))
return property(fget=fget, doc=doc) return property(fget=fget, doc=doc)
def __init__(self, node, mountpath, tags, prefix=""): def __init__(self, node, mountpath, tags, prefix=""):
self.elem = node self.elem = node
self.prefix = prefix self.prefix = prefix
self.tags = tags self.tags = tags
self.mountpath = mountpath self.mountpath = mountpath
def __str__(self): def __str__(self):
""" Return a utf-8 encoded string with title author and path information """ """ Return a utf-8 encoded string with title author and path information """
return self.title.encode('utf-8') + " by " + \ return self.title.encode('utf-8') + " by " + \
@ -128,7 +128,7 @@ class Book(object):
class BookList(_BookList): class BookList(_BookList):
def __init__(self, xml_file, mountpath, report_progress=None): def __init__(self, xml_file, mountpath, report_progress=None):
_BookList.__init__(self) _BookList.__init__(self)
xml_file.seek(0) xml_file.seek(0)
@ -143,15 +143,15 @@ class BookList(_BookList):
self.root_element = records[0] self.root_element = records[0]
else: else:
self.prefix = '' self.prefix = ''
nodes = self.root_element.childNodes nodes = self.root_element.childNodes
for i, book in enumerate(nodes): for i, book in enumerate(nodes):
if report_progress: if report_progress:
self.report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...')) report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...'))
if hasattr(book, 'tagName') and book.tagName.endswith('text'): if hasattr(book, 'tagName') and book.tagName.endswith('text'):
tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))] tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))]
self.append(Book(book, mountpath, tags, prefix=self.prefix)) self.append(Book(book, mountpath, tags, prefix=self.prefix))
def max_id(self): def max_id(self):
max = 0 max = 0
for child in self.root_element.childNodes: for child in self.root_element.childNodes:
@ -160,7 +160,7 @@ class BookList(_BookList):
if nid > max: if nid > max:
max = nid max = nid
return max return max
def is_id_valid(self, id): def is_id_valid(self, id):
'''Return True iff there is an element with C{id==id}.''' '''Return True iff there is an element with C{id==id}.'''
id = str(id) id = str(id)
@ -169,23 +169,23 @@ class BookList(_BookList):
if child.getAttribute('id') == id: if child.getAttribute('id') == id:
return True return True
return False return False
def supports_tags(self): def supports_tags(self):
return True return True
def book_by_path(self, path): def book_by_path(self, path):
for child in self.root_element.childNodes: for child in self.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"): if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
if path == child.getAttribute('path'): if path == child.getAttribute('path'):
return child return child
return None return None
def add_book(self, info, name, size, ctime): def add_book(self, info, name, size, ctime):
""" Add a node into the DOM tree, representing a book """ """ Add a node into the DOM tree, representing a book """
book = self.book_by_path(name) book = self.book_by_path(name)
if book is not None: if book is not None:
self.remove_book(name) self.remove_book(name)
node = self.document.createElement(self.prefix + "text") node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name.rpartition('.')[-1].lower()] mime = MIME_MAP[name.rpartition('.')[-1].lower()]
cid = self.max_id()+1 cid = self.max_id()+1
@ -194,23 +194,23 @@ class BookList(_BookList):
except: except:
sourceid = '1' sourceid = '1'
attrs = { attrs = {
"title" : info["title"], "title" : info["title"],
'titleSorter' : sortable_title(info['title']), 'titleSorter' : sortable_title(info['title']),
"author" : info["authors"] if info['authors'] else _('Unknown'), "author" : info["authors"] if info['authors'] else _('Unknown'),
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size) "mime":mime, "path":name, "size":str(size)
} }
for attr in attrs.keys(): for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr)) node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr]) node.setAttribute(attr, attrs[attr])
try: try:
w, h, data = info["cover"] w, h, data = info["cover"]
except TypeError: except TypeError:
w, h, data = None, None, None w, h, data = None, None, None
if data: if data:
th = self.document.createElement(self.prefix + "thumbnail") th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w)) th.setAttribute("width", str(w))
th.setAttribute("height", str(h)) th.setAttribute("height", str(h))
jpeg = self.document.createElement(self.prefix + "jpeg") jpeg = self.document.createElement(self.prefix + "jpeg")
@ -225,24 +225,24 @@ class BookList(_BookList):
if info.has_key('tag order'): if info.has_key('tag order'):
self.tag_order.update(info['tag order']) self.tag_order.update(info['tag order'])
self.set_tags(book, info['tags']) self.set_tags(book, info['tags'])
def _delete_book(self, node): def _delete_book(self, node):
nid = node.getAttribute('id') nid = node.getAttribute('id')
self.remove_from_playlists(nid) self.remove_from_playlists(nid)
node.parentNode.removeChild(node) node.parentNode.removeChild(node)
node.unlink() node.unlink()
def delete_book(self, cid): def delete_book(self, cid):
''' '''
Remove DOM node corresponding to book with C{id == cid}. Remove DOM node corresponding to book with C{id == cid}.
Also remove book from any collections it is part of. Also remove book from any collections it is part of.
''' '''
for book in self: for book in self:
if str(book.id) == str(cid): if str(book.id) == str(cid):
self.remove(book) self.remove(book)
self._delete_book(book.elem) self._delete_book(book.elem)
break break
def remove_book(self, path): def remove_book(self, path):
''' '''
Remove DOM node corresponding to book with C{path == path}. Remove DOM node corresponding to book with C{path == path}.
@ -251,24 +251,24 @@ class BookList(_BookList):
for book in self: for book in self:
if path.endswith(book.rpath): if path.endswith(book.rpath):
self.remove(book) self.remove(book)
self._delete_book(book.elem) self._delete_book(book.elem)
break break
def playlists(self): def playlists(self):
ans = [] ans = []
for c in self.root_element.childNodes: for c in self.root_element.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('playlist'): if hasattr(c, 'tagName') and c.tagName.endswith('playlist'):
ans.append(c) ans.append(c)
return ans return ans
def playlist_items(self): def playlist_items(self):
plitems = [] plitems = []
for pl in self.playlists(): for pl in self.playlists():
for c in pl.childNodes: for c in pl.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('item'): if hasattr(c, 'tagName') and c.tagName.endswith('item'):
plitems.append(c) plitems.append(c)
return plitems return plitems
def purge_corrupted_files(self): def purge_corrupted_files(self):
if not self.root_element: if not self.root_element:
return [] return []
@ -279,7 +279,7 @@ class BookList(_BookList):
c.parentNode.removeChild(c) c.parentNode.removeChild(c)
c.unlink() c.unlink()
return paths return paths
def purge_empty_playlists(self): def purge_empty_playlists(self):
''' Remove all playlists that have no children. Also removes any invalid playlist items.''' ''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
for pli in self.playlist_items(): for pli in self.playlist_items():
@ -298,32 +298,32 @@ class BookList(_BookList):
if empty: if empty:
pl.parentNode.removeChild(pl) pl.parentNode.removeChild(pl)
pl.unlink() pl.unlink()
def playlist_by_title(self, title): def playlist_by_title(self, title):
for pl in self.playlists(): for pl in self.playlists():
if pl.getAttribute('title').lower() == title.lower(): if pl.getAttribute('title').lower() == title.lower():
return pl return pl
def add_playlist(self, title): def add_playlist(self, title):
cid = self.max_id()+1 cid = self.max_id()+1
pl = self.document.createElement(self.prefix+'playlist') pl = self.document.createElement(self.prefix+'playlist')
pl.setAttribute('id', str(cid)) pl.setAttribute('id', str(cid))
pl.setAttribute('title', title) pl.setAttribute('title', title)
pl.setAttribute('uuid', uuid()) pl.setAttribute('uuid', uuid())
self.root_element.insertBefore(pl, self.root_element.childNodes[-1]) self.root_element.insertBefore(pl, self.root_element.childNodes[-1])
return pl return pl
def remove_from_playlists(self, id): def remove_from_playlists(self, id):
for pli in self.playlist_items(): for pli in self.playlist_items():
if pli.getAttribute('id') == str(id): if pli.getAttribute('id') == str(id):
pli.parentNode.removeChild(pli) pli.parentNode.removeChild(pli)
pli.unlink() pli.unlink()
def set_tags(self, book, tags): def set_tags(self, book, tags):
tags = [t for t in tags if t] tags = [t for t in tags if t]
book.tags = tags book.tags = tags
self.set_playlists(book.id, tags) self.set_playlists(book.id, tags)
def set_playlists(self, id, collections): def set_playlists(self, id, collections):
self.remove_from_playlists(id) self.remove_from_playlists(id)
for collection in set(collections): for collection in set(collections):
@ -333,7 +333,7 @@ class BookList(_BookList):
item = self.document.createElement(self.prefix+'item') item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(id)) item.setAttribute('id', str(id))
coll.appendChild(item) coll.appendChild(item)
def get_playlists(self, bookid): def get_playlists(self, bookid):
ans = [] ans = []
for pl in self.playlists(): for pl in self.playlists():
@ -342,23 +342,23 @@ class BookList(_BookList):
if item.getAttribute('id') == str(bookid): if item.getAttribute('id') == str(bookid):
ans.append(pl) ans.append(pl)
return ans return ans
def next_id(self): def next_id(self):
return self.document.documentElement.getAttribute('nextID') return self.document.documentElement.getAttribute('nextID')
def set_next_id(self, id): def set_next_id(self, id):
self.document.documentElement.setAttribute('nextID', str(id)) self.document.documentElement.setAttribute('nextID', str(id))
def write(self, stream): def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """ """ Write XML representation of DOM tree to C{stream} """
src = self.document.toxml('utf-8') + '\n' src = self.document.toxml('utf-8') + '\n'
stream.write(src.replace("'", '&apos;')) stream.write(src.replace("'", '&apos;'))
def book_by_id(self, id): def book_by_id(self, id):
for book in self: for book in self:
if str(book.id) == str(id): if str(book.id) == str(id):
return book return book
def reorder_playlists(self): def reorder_playlists(self):
for title in self.tag_order.keys(): for title in self.tag_order.keys():
pl = self.playlist_by_title(title) pl = self.playlist_by_title(title)
@ -371,7 +371,7 @@ class BookList(_BookList):
map[i] = j map[i] = j
pl_book_ids = [i for i in pl_book_ids if i is not None] pl_book_ids = [i for i in pl_book_ids if i is not None]
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids] ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
if len(ordered_ids) < len(pl.childNodes): if len(ordered_ids) < len(pl.childNodes):
continue continue
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')] children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
@ -382,7 +382,7 @@ class BookList(_BookList):
item = self.document.createElement(self.prefix+'item') item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(map[id])) item.setAttribute('id', str(map[id]))
pl.appendChild(item) pl.appendChild(item)
def fix_ids(main, carda, cardb): def fix_ids(main, carda, cardb):
''' '''
Adjust ids the XML databases. Adjust ids the XML databases.
@ -393,7 +393,7 @@ def fix_ids(main, carda, cardb):
carda.purge_empty_playlists() carda.purge_empty_playlists()
if hasattr(cardb, 'purge_empty_playlists'): if hasattr(cardb, 'purge_empty_playlists'):
cardb.purge_empty_playlists() cardb.purge_empty_playlists()
def regen_ids(db): def regen_ids(db):
if not hasattr(db, 'root_element'): if not hasattr(db, 'root_element'):
return return
@ -402,11 +402,11 @@ def fix_ids(main, carda, cardb):
cid = 0 if db == main else 1 cid = 0 if db == main else 1
for child in db.root_element.childNodes: for child in db.root_element.childNodes:
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute('id'): if child.nodeType == child.ELEMENT_NODE and child.hasAttribute('id'):
id_map[child.getAttribute('id')] = str(cid) id_map[child.getAttribute('id')] = str(cid)
child.setAttribute("sourceid", '1') child.setAttribute("sourceid", '1')
child.setAttribute('id', str(cid)) child.setAttribute('id', str(cid))
cid += 1 cid += 1
for item in db.playlist_items(): for item in db.playlist_items():
oid = item.getAttribute('id') oid = item.getAttribute('id')
try: try:
@ -414,11 +414,11 @@ def fix_ids(main, carda, cardb):
except KeyError: except KeyError:
item.parentNode.removeChild(item) item.parentNode.removeChild(item)
item.unlink() item.unlink()
db.reorder_playlists() db.reorder_playlists()
regen_ids(main) regen_ids(main)
regen_ids(carda) regen_ids(carda)
regen_ids(cardb) regen_ids(cardb)
main.set_next_id(str(main.max_id()+1)) main.set_next_id(str(main.max_id()+1))

View File

@ -12,7 +12,8 @@ class DeviceConfig(object):
@classmethod @classmethod
def _config(cls): def _config(cls):
c = Config('device_drivers_%s' % cls.__class__.__name__, _('settings for device drivers')) klass = cls if isinstance(cls, type) else cls.__class__
c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers'))
c.add_opt('format_map', default=cls.FORMATS, help=cls.HELP_MESSAGE) c.add_opt('format_map', default=cls.FORMATS, help=cls.HELP_MESSAGE)
return c return c
@ -33,7 +34,7 @@ class DeviceConfig(object):
@classmethod @classmethod
def settings(cls): def settings(cls):
return cls._config().parse() return cls._config().parse()
def customization_help(cls, gui=False): def customization_help(cls, gui=False):
return cls.HELP_MESSAGE return cls.HELP_MESSAGE

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.utils.config import config_dir
from calibre.utils.lock import ExclusiveFile
from calibre import sanitize_file_name
from calibre.customize.conversion import OptionRecommendation
config_dir = os.path.join(config_dir, 'conversion')
if not os.path.exists(config_dir):
os.makedirs(config_dir)
def name_to_path(name):
return os.path.join(config_dir, sanitize_file_name(name)+'.py')
def save_defaults(name, recs):
path = name_to_path(name)
raw = str(recs)
with open(path, 'wb'):
pass
with ExclusiveFile(path) as f:
f.write(raw)
def load_defaults(name):
path = name_to_path(name)
if not os.path.exists(path):
open(path, 'wb').close()
with ExclusiveFile(path) as f:
raw = f.read()
r = GuiRecommendations()
if raw:
r.from_string(raw)
return r
def save_specifics(db, book_id, recs):
raw = str(recs)
db.set_conversion_options(book_id, 'PIPE', raw)
def load_specifics(db, book_id):
raw = db.conversion_options(book_id, 'PIPE')
r = GuiRecommendations()
if raw:
r.from_string(raw)
return r
class GuiRecommendations(dict):
def __new__(cls, *args):
dict.__new__(cls)
obj = super(GuiRecommendations, cls).__new__(cls, *args)
obj.disabled_options = set([])
return obj
def to_recommendations(self, level=OptionRecommendation.LOW):
ans = []
for key, val in self.items():
ans.append((key, val, level))
return ans
def __str__(self):
ans = ['{']
for key, val in self.items():
ans.append('\t'+repr(key)+' : '+repr(val)+',')
ans.append('}')
return '\n'.join(ans)
def from_string(self, raw):
try:
d = eval(raw)
except SyntaxError:
d = None
if d:
self.update(d)
def merge_recommendations(self, get_option, level, options,
only_existing=False):
for name in options:
if only_existing and name not in self:
continue
opt = get_option(name)
if opt is None: continue
if opt.level == OptionRecommendation.HIGH:
self[name] = opt.recommended_value
self.disabled_options.add(name)
elif opt.level > level or name not in self:
self[name] = opt.recommended_value

View File

@ -241,8 +241,13 @@ class MetaInformation(object):
self.tags += mi.tags self.tags += mi.tags
self.tags = list(set(self.tags)) self.tags = list(set(self.tags))
if getattr(mi, 'cover_data', None) and mi.cover_data[0] is not None: if getattr(mi, 'cover_data', False):
self.cover_data = mi.cover_data other_cover = mi.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
if not self_cover: self_cover = ''
if not other_cover: other_cover = ''
if len(other_cover) > len(self_cover):
self.cover_data = mi.cover_data
my_comments = getattr(self, 'comments', '') my_comments = getattr(self, 'comments', '')
other_comments = getattr(mi, 'comments', '') other_comments = getattr(mi, 'comments', '')

View File

@ -9,12 +9,9 @@ __copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net and ' \
'Marshall T. Vandegrift <llasram@gmail.com>' 'Marshall T. Vandegrift <llasram@gmail.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys
import os
from struct import pack, unpack from struct import pack, unpack
from cStringIO import StringIO from cStringIO import StringIO
from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.reader import get_metadata
from calibre.ebooks.mobi.writer import rescale_image, MAX_THUMB_DIMEN from calibre.ebooks.mobi.writer import rescale_image, MAX_THUMB_DIMEN
from calibre.ebooks.mobi.langcodes import iana2mobi from calibre.ebooks.mobi.langcodes import iana2mobi
@ -116,8 +113,13 @@ class MetadataUpdater(object):
def update(self, mi): def update(self, mi):
recs = [] recs = []
from calibre.ebooks.mobi.from_any import config try:
if mi.author_sort and config().parse().prefer_author_sort: from calibre.ebooks.conversion.config import load_defaults
prefs = load_defaults('mobi_output')
pas = prefs.get('prefer_author_sort', False)
except:
pas = False
if mi.author_sort and pas:
authors = mi.author_sort authors = mi.author_sort
recs.append((100, authors.encode(self.codec, 'replace'))) recs.append((100, authors.encode(self.codec, 'replace')))
elif mi.authors: elif mi.authors:

View File

@ -0,0 +1,111 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Thread
from Queue import Empty
import os, time
from calibre.utils.ipc.job import ParallelJob
from calibre.utils.ipc.server import Server
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre import prints
def read_metadata_(task, tdir, notification=lambda x,y:x):
from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.ebooks.metadata.opf2 import OPFCreator
for x in task:
id, formats = x
if isinstance(formats, basestring): formats = [formats]
mi = metadata_from_formats(formats)
mi.cover = None
cdata = None
if mi.cover_data:
cdata = mi.cover_data[-1]
mi.cover_data = None
opf = OPFCreator(tdir, mi)
with open(os.path.join(tdir, '%s.opf'%id), 'wb') as f:
opf.render(f)
if cdata:
with open(os.path.join(tdir, str(id)), 'wb') as f:
f.write(cdata)
notification(0.5, id)
class Progress(object):
def __init__(self, result_queue, tdir):
self.result_queue = result_queue
self.tdir = tdir
def __call__(self, id):
cover = os.path.join(self.tdir, str(id))
if not os.path.exists(cover): cover = None
self.result_queue.put((id, os.path.join(self.tdir, id+'.opf'), cover))
class ReadMetadata(Thread):
def __init__(self, tasks, result_queue):
self.tasks, self.result_queue = tasks, result_queue
self.canceled = False
Thread.__init__(self)
self.daemon = True
self.tdir = PersistentTemporaryDirectory('_rm_worker')
def run(self):
jobs, ids = set([]), set([id for id, p in self.tasks])
progress = Progress(self.result_queue, self.tdir)
server = Server()
for i, task in enumerate(self.tasks):
job = ParallelJob('read_metadata',
'Read metadata (%d of %d)'%(i, len(self.tasks)),
lambda x,y:x, args=[task, self.tdir])
jobs.add(job)
server.add_job(job)
while not self.canceled:
time.sleep(0.2)
running = False
for job in jobs:
while True:
try:
id = job.notifications.get_nowait()[-1]
progress(id)
ids.remove(id)
except Empty:
break
job.update()
if not job.is_finished:
running = True
if not running:
break
if self.canceled:
server.close()
time.sleep(1)
return
for id in ids:
progress(id)
for job in jobs:
if job.failed:
prints(job.details)
if os.path.exists(job.log_path):
os.remove(job.log_path)
def read_metadata(paths, result_queue):
tasks = []
chunk = 50
pos = 0
while pos < len(paths):
tasks.append(paths[pos:pos+chunk])
pos += chunk
t = ReadMetadata(tasks, result_queue)
t.start()
return t

View File

@ -2,238 +2,152 @@
UI for adding books to the database UI for adding books to the database
''' '''
import os import os
from threading import Queue, Empty
from PyQt4.Qt import QThread, SIGNAL, QMutex, QWaitCondition, Qt from PyQt4.Qt import QThread, SIGNAL, QObject, QTimer
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.constants import preferred_encoding
from calibre.gui2 import warning_dialog from calibre.gui2 import warning_dialog
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ebooks.metadata import MetaInformation
from calibre.constants import preferred_encoding
class Add(QThread): class RecursiveFind(QThread):
def __init__(self): def __init__(self, parent, db, root, single):
QThread.__init__(self) QThread.__init__(self, parent)
self._lock = QMutex()
self._waiting = QWaitCondition()
def is_canceled(self):
if self.pd.canceled:
self.canceled = True
return self.canceled
def wait_for_condition(self):
self._lock.lock()
self._waiting.wait(self._lock)
self._lock.unlock()
def wake_up(self):
self._waiting.wakeAll()
class AddFiles(Add):
def __init__(self, paths, default_thumbnail, get_metadata, db=None):
Add.__init__(self)
self.paths = paths
self.get_metadata = get_metadata
self.default_thumbnail = default_thumbnail
self.db = db self.db = db
self.formats, self.metadata, self.names, self.infos = [], [], [], [] self.path = root, self.single_book_per_directory = single
self.duplicates = [] self.canceled = False
def run(self):
root = os.path.abspath(self.path)
for dirpath in os.walk(root):
if self.canceled:
return
self.emit(SIGNAL('update(PyQt_PyObject)'),
_('Searching in')+' '+dirpath[0])
self.books += list(self.db.find_books_in_directory(dirpath[0],
self.single_book_per_directory))
self.books = [formats for formats in self.books if formats]
if not self.canceled:
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
class Adder(QObject):
def __init__(self, parent, db, callback):
QObject.__init__(self, parent)
self.pd = ProgressDialog(_('Add books'), parent=parent)
self.db = db
self.pd.setModal(True)
self.pd.show()
self._parent = parent
self.number_of_books_added = 0 self.number_of_books_added = 0
self.connect(self.get_metadata, self.rfind = self.worker = self.timer = None
SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'), self.callback = callback
self.metadata_delivered) self.infos, self.paths, self.names = [], [], []
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
def metadata_delivered(self, id, mi):
if self.is_canceled(): def add_recursive(self, root, single=True):
self.wake_up() self.path = root
self.pd.set_msg(_('Searching for books in all sub-directories...'))
self.pd.set_min(0)
self.pd.set_max(0)
self.pd.value = 0
self.rfind = RecursiveFind(self, self.db, root, single)
self.connect(self.rfind, SIGNAL('update(PyQt_PyObject)'),
self.pd.set_msg)
self.connect(self.rfind, SIGNAL('found(PyQt_PyObject)'),
self.add)
def add(self, books):
books = [[b] if isinstance(b, basestring) else b for b in books]
self.rfind = None
from calibre.ebooks.metadata.worker import read_metadata
self.rq = Queue()
tasks = []
self.ids = {}
self.nmap = {}
self.duplicates = []
for i, b in books:
tasks.append((i, b))
self.ids[i] = b
self.nmap = os.path.basename(b[0])
self.worker = read_metadata(tasks, self.rq)
self.pd.set_min(0)
self.pd.set_max(len(self.ids))
self.pd.value = 0
self.timer = QTimer(self)
self.connect(self.timer, SIGNAL('timeout()'), self.update)
self.timer.start(200)
def add_formats(self, id, formats):
for path in formats:
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
self.db.add_format(id, fmt, open(path, 'rb'), index_is_id=True,
notify=False)
def canceled(self):
if self.rfind is not None:
self.rfind.cenceled = True
if self.timer is not None:
self.timer.stop()
if self.worker is not None:
self.worker.canceled = True
self.pd.hide()
def update(self):
if not self.ids:
self.timer.stop()
self.process_duplicates()
self.pd.hide()
self.callback(self.paths, self.names, self.infos)
return return
try:
id, opf, cover = self.rq.get_nowait()
except Empty:
return
self.pd.value += 1
formats = self.ids.pop(id)
mi = MetaInformation(OPF(opf))
name = self.nmap.pop(id)
if not mi.title: if not mi.title:
mi.title = os.path.splitext(self.names[id])[0] mi.title = os.path.splitext(name)[0]
mi.title = mi.title if isinstance(mi.title, unicode) else \ mi.title = mi.title if isinstance(mi.title, unicode) else \
mi.title.decode(preferred_encoding, 'replace') mi.title.decode(preferred_encoding, 'replace')
self.metadata.append(mi) self.pd.set_msg(_('Added')+' '+mi.title)
self.infos.append({'title':mi.title,
'authors':', '.join(mi.authors),
'cover':self.default_thumbnail, 'tags':[]})
if self.db is not None: if self.db is not None:
duplicates, num = self.db.add_books(self.paths[id:id+1], if cover:
self.formats[id:id+1], [mi], cover = open(cover, 'rb').read()
add_duplicates=False) id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
self.number_of_books_added += num self.number_of_books_added += 1
if duplicates: if id is None:
if not self.duplicates: self.duplicates.append((mi, cover, formats))
self.duplicates = [[], [], [], []] else:
for i in range(4): self.add_formats(id, formats)
self.duplicates[i] += duplicates[i]
self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
mi.title, id)
self.wake_up()
def create_progress_dialog(self, title, msg, parent):
self._parent = parent
self.pd = ProgressDialog(title, msg, -1, len(self.paths)-1, parent)
self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
self.update_progress_dialog)
self.pd.setModal(True)
self.pd.show()
self.connect(self, SIGNAL('finished()'), self.pd.hide)
return self.pd
def update_progress_dialog(self, title, count):
self.pd.set_value(count)
if self.db is not None:
self.pd.set_msg(_('Added %s to library')%title)
else: else:
self.pd.set_msg(_('Read metadata from ')+title) self.names.append(name)
self.paths.append(formats[0])
self.infos.append({'title':mi.title,
def run(self): 'authors':', '.join(mi.authors),
try: 'cover':None,
self.canceled = False 'tags':mi.tags if mi.tags else []})
for c, book in enumerate(self.paths):
if self.pd.canceled:
self.canceled = True
break
format = os.path.splitext(book)[1]
format = format[1:] if format else None
stream = open(book, 'rb')
self.formats.append(format)
self.names.append(os.path.basename(book))
self.get_metadata(c, stream, stream_type=format,
use_libprs_metadata=True)
self.wait_for_condition()
finally:
self.disconnect(self.get_metadata,
SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'),
self.metadata_delivered)
self.get_metadata = None
def process_duplicates(self): def process_duplicates(self):
if self.duplicates: files = [x[0].title for x in self.duplicates]
files = '' d = warning_dialog(_('Duplicates found!'),
for mi in self.duplicates[2]:
files += mi.title+'\n'
d = warning_dialog(_('Duplicates found!'),
_('Books with the same title as the following already '
'exist in the database. Add them anyway?'),
files, parent=self._parent)
if d.exec_() == d.Accepted:
num = self.db.add_books(*self.duplicates,
**dict(add_duplicates=True))[1]
self.number_of_books_added += num
class AddRecursive(Add):
def __init__(self, path, db, get_metadata, single_book_per_directory, parent):
self.path = path
self.db = db
self.get_metadata = get_metadata
self.single_book_per_directory = single_book_per_directory
self.duplicates, self.books, self.metadata = [], [], []
self.number_of_books_added = 0
self.canceled = False
Add.__init__(self)
self.connect(self.get_metadata,
SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'),
self.metadata_delivered, Qt.QueuedConnection)
self.connect(self, SIGNAL('searching_done()'), self.searching_done,
Qt.QueuedConnection)
self._parent = parent
self.pd = ProgressDialog(_('Adding books recursively...'),
_('Searching for books in all sub-directories...'),
0, 0, parent)
self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
self.update_progress_dialog)
self.connect(self, SIGNAL('update(PyQt_PyObject)'), self.pd.set_msg,
Qt.QueuedConnection)
self.connect(self, SIGNAL('pupdate(PyQt_PyObject)'), self.pd.set_value,
Qt.QueuedConnection)
self.pd.setModal(True)
self.pd.show()
self.connect(self, SIGNAL('finished()'), self.pd.hide)
def update_progress_dialog(self, title, count):
self.pd.set_value(count)
if title:
self.pd.set_msg(_('Read metadata from ')+title)
def metadata_delivered(self, id, mi):
if self.is_canceled():
self.wake_up()
return
self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
mi.title, id)
self.metadata.append((mi if mi.title else None, self.books[id]))
if len(self.metadata) >= len(self.books):
self.metadata = [x for x in self.metadata if x[0] is not None]
self.pd.set_min(-1)
self.pd.set_max(len(self.metadata)-1)
self.pd.set_value(-1)
self.pd.set_msg(_('Adding books to database...'))
self.wake_up()
def searching_done(self):
self.pd.set_min(-1)
self.pd.set_max(len(self.books)-1)
self.pd.set_value(-1)
self.pd.set_msg(_('Reading metadata...'))
def run(self):
try:
root = os.path.abspath(self.path)
for dirpath in os.walk(root):
if self.is_canceled():
return
self.emit(SIGNAL('update(PyQt_PyObject)'),
_('Searching in')+' '+dirpath[0])
self.books += list(self.db.find_books_in_directory(dirpath[0],
self.single_book_per_directory))
self.books = [formats for formats in self.books if formats]
# Reset progress bar
self.emit(SIGNAL('searching_done()'))
for c, formats in enumerate(self.books):
self.get_metadata.from_formats(c, formats)
self.wait_for_condition()
# Add books to database
for c, x in enumerate(self.metadata):
mi, formats = x
if self.is_canceled():
break
if self.db.has_book(mi):
self.duplicates.append((mi, formats))
else:
self.db.import_book(mi, formats, notify=False)
self.number_of_books_added += 1
self.emit(SIGNAL('pupdate(PyQt_PyObject)'), c)
finally:
self.disconnect(self.get_metadata,
SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'),
self.metadata_delivered)
self.get_metadata = None
def process_duplicates(self):
if self.duplicates:
files = ''
for mi in self.duplicates:
title = mi[0].title
if not isinstance(title, unicode):
title = title.decode(preferred_encoding, 'replace')
files += title+'\n'
d = warning_dialog(_('Duplicates found!'),
_('Books with the same title as the following already ' _('Books with the same title as the following already '
'exist in the database. Add them anyway?'), 'exist in the database. Add them anyway?'),
files, parent=self._parent) '\n'.join(files), parent=self._parent)
if d.exec_() == d.Accepted: if d.exec_() == d.Accepted:
for mi, formats in self.duplicates: for mi, cover, formats in self.duplicates:
self.db.import_book(mi, formats, notify=False) id = self.db.create_book_entry(mi, cover=cover,
self.number_of_books_added += 1 add_duplicates=False)
self.add_formats(id, formats)
self.number_of_books_added += 1

View File

@ -6,96 +6,13 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
QCheckBox, QComboBox, Qt, QIcon, SIGNAL QCheckBox, QComboBox, Qt, QIcon, SIGNAL
from calibre.customize.conversion import OptionRecommendation from calibre.customize.conversion import OptionRecommendation
from calibre.utils.config import config_dir from calibre.ebooks.conversion.config import load_defaults, \
from calibre.utils.lock import ExclusiveFile save_defaults as save_defaults_, \
from calibre import sanitize_file_name load_specifics, GuiRecommendations
config_dir = os.path.join(config_dir, 'conversion')
if not os.path.exists(config_dir):
os.makedirs(config_dir)
def name_to_path(name):
return os.path.join(config_dir, sanitize_file_name(name)+'.py')
def save_defaults(name, recs):
path = name_to_path(name)
raw = str(recs)
with open(path, 'wb'):
pass
with ExclusiveFile(path) as f:
f.write(raw)
save_defaults_ = save_defaults
def load_defaults(name):
path = name_to_path(name)
if not os.path.exists(path):
open(path, 'wb').close()
with ExclusiveFile(path) as f:
raw = f.read()
r = GuiRecommendations()
if raw:
r.from_string(raw)
return r
def save_specifics(db, book_id, recs):
raw = str(recs)
db.set_conversion_options(book_id, 'PIPE', raw)
def load_specifics(db, book_id):
raw = db.conversion_options(book_id, 'PIPE')
r = GuiRecommendations()
if raw:
r.from_string(raw)
return r
class GuiRecommendations(dict):
def __new__(cls, *args):
dict.__new__(cls)
obj = super(GuiRecommendations, cls).__new__(cls, *args)
obj.disabled_options = set([])
return obj
def to_recommendations(self, level=OptionRecommendation.LOW):
ans = []
for key, val in self.items():
ans.append((key, val, level))
return ans
def __str__(self):
ans = ['{']
for key, val in self.items():
ans.append('\t'+repr(key)+' : '+repr(val)+',')
ans.append('}')
return '\n'.join(ans)
def from_string(self, raw):
try:
d = eval(raw)
except SyntaxError:
d = None
if d:
self.update(d)
def merge_recommendations(self, get_option, level, options,
only_existing=False):
for name in options:
if only_existing and name not in self:
continue
opt = get_option(name)
if opt is None: continue
if opt.level == OptionRecommendation.HIGH:
self[name] = opt.recommended_value
self.disabled_options.add(name)
elif opt.level > level or name not in self:
self[name] = opt.recommended_value
class Widget(QWidget): class Widget(QWidget):

View File

@ -11,7 +11,7 @@ import sys, cPickle
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
from calibre.gui2 import ResizableDialog, NONE from calibre.gui2 import ResizableDialog, NONE
from calibre.gui2.convert import GuiRecommendations, save_specifics, \ from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics, \
load_specifics load_specifics
from calibre.gui2.convert.single_ui import Ui_Dialog from calibre.gui2.convert.single_ui import Ui_Dialog
from calibre.gui2.convert.metadata import MetadataWidget from calibre.gui2.convert.metadata import MetadataWidget

View File

@ -34,18 +34,18 @@ class DeviceJob(BaseJob):
BaseJob.__init__(self, description, done=done) BaseJob.__init__(self, description, done=done)
self.func = func self.func = func
self.args, self.kwargs = args, kwargs self.args, self.kwargs = args, kwargs
self.exception = None
self.job_manager = job_manager self.job_manager = job_manager
self.job_manager.add_job(self) self._details = _('No details available.')
self.details = _('No details available.')
def start_work(self): def start_work(self):
self.start_time = time.time() self.start_time = time.time()
self.job_manager.changed_queue.put(self) self.job_manager.changed_queue.put(self)
def job_done(self): def job_done(self):
self.duration = time.time() - self.start_time() self.duration = time.time() - self.start_time
self.percent = 1
self.job_manager.changed_queue.put(self) self.job_manager.changed_queue.put(self)
self.job_manager.job_done(self)
def report_progress(self, percent, msg=''): def report_progress(self, percent, msg=''):
self.notifications.put((percent, msg)) self.notifications.put((percent, msg))
@ -57,7 +57,7 @@ class DeviceJob(BaseJob):
self.result = self.func(*self.args, **self.kwargs) self.result = self.func(*self.args, **self.kwargs)
except (Exception, SystemExit), err: except (Exception, SystemExit), err:
self.failed = True self.failed = True
self.details = unicode(err) + '\n\n' + \ self._details = unicode(err) + '\n\n' + \
traceback.format_exc() traceback.format_exc()
self.exception = err self.exception = err
finally: finally:
@ -65,7 +65,7 @@ class DeviceJob(BaseJob):
@property @property
def log_file(self): def log_file(self):
return cStringIO.StringIO(self.details.encode('utf-8')) return cStringIO.StringIO(self._details.encode('utf-8'))
class DeviceManager(Thread): class DeviceManager(Thread):
@ -230,7 +230,6 @@ class DeviceManager(Thread):
def _view_book(self, path, target): def _view_book(self, path, target):
f = open(target, 'wb') f = open(target, 'wb')
print self.device
self.device.get_file(path, f) self.device.get_file(path, f)
f.close() f.close()
return target return target
@ -379,12 +378,12 @@ class DeviceMenu(QMenu):
if action.dest == 'main:': if action.dest == 'main:':
action.setEnabled(True) action.setEnabled(True)
elif action.dest == 'carda:0': elif action.dest == 'carda:0':
if card_prefix[0] != None: if card_prefix and card_prefix[0] != None:
action.setEnabled(True) action.setEnabled(True)
else: else:
action.setEnabled(False) action.setEnabled(False)
elif action.dest == 'cardb:0': elif action.dest == 'cardb:0':
if card_prefix[1] != None: if card_prefix and card_prefix[1] != None:
action.setEnabled(True) action.setEnabled(True)
else: else:
action.setEnabled(False) action.setEnabled(False)
@ -737,7 +736,7 @@ class DeviceGUI(object):
''' '''
Called once metadata has been uploaded. Called once metadata has been uploaded.
''' '''
if job.exception is not None: if job.failed:
self.device_job_exception(job) self.device_job_exception(job)
return return
cp, fs = job.result cp, fs = job.result

View File

@ -10,7 +10,7 @@ from PyQt4.Qt import QDialog, SIGNAL, Qt
from calibre.gui2.dialogs.progress_ui import Ui_Dialog from calibre.gui2.dialogs.progress_ui import Ui_Dialog
class ProgressDialog(QDialog, Ui_Dialog): class ProgressDialog(QDialog, Ui_Dialog):
def __init__(self, title, msg='', min=0, max=99, parent=None): def __init__(self, title, msg='', min=0, max=99, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
@ -22,28 +22,39 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.set_max(max) self.set_max(max)
self.bar.setValue(min) self.bar.setValue(min)
self.canceled = False self.canceled = False
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled) self.connect(self.button_box, SIGNAL('rejected()'), self._canceled)
def set_msg(self, msg=''): def set_msg(self, msg=''):
self.message.setText(msg) self.message.setText(msg)
def set_value(self, val): def set_value(self, val):
self.bar.setValue(val) self.bar.setValue(val)
@dynamic_property
def value(self):
def fset(self, val):
return self.bar.setValue(val)
def fget(self):
return self.bar.value()
return property(fget=fget, fset=fset)
def set_min(self, min): def set_min(self, min):
self.bar.setMinimum(min) self.bar.setMinimum(min)
def set_max(self, max): def set_max(self, max):
self.bar.setMaximum(max) self.bar.setMaximum(max)
def _canceled(self, *args): def _canceled(self, *args):
self.canceled = True self.canceled = True
self.button_box.setDisabled(True) self.button_box.setDisabled(True)
self.title.setText(_('Aborting...')) self.title.setText(_('Aborting...'))
self.emit(SIGNAL('canceled()'))
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Escape: if ev.key() == Qt.Key_Escape:
self._canceled() self._canceled()
else: else:
QDialog.keyPressEvent(self, ev) QDialog.keyPressEvent(self, ev)

View File

@ -31,8 +31,7 @@ class JobManager(QAbstractTableModel):
self.jobs = [] self.jobs = []
self.add_job = Dispatcher(self._add_job) self.add_job = Dispatcher(self._add_job)
self.job_done = Dispatcher(self._job_done) self.server = Server()
self.server = Server(self.job_done)
self.changed_queue = Queue() self.changed_queue = Queue()
self.timer = QTimer(self) self.timer = QTimer(self)
@ -98,7 +97,8 @@ class JobManager(QAbstractTableModel):
try: try:
self._update() self._update()
except BaseException: except BaseException:
pass import traceback
traceback.print_exc()
def _update(self): def _update(self):
# Update running time # Update running time
@ -132,6 +132,8 @@ class JobManager(QAbstractTableModel):
if needs_reset: if needs_reset:
self.jobs.sort() self.jobs.sort()
self.reset() self.reset()
if job.is_finished:
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
else: else:
for job in jobs: for job in jobs:
idx = self.jobs.index(job) idx = self.jobs.index(job)
@ -155,12 +157,6 @@ class JobManager(QAbstractTableModel):
def row_to_job(self, row): def row_to_job(self, row):
return self.jobs[row] return self.jobs[row]
def _job_done(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()'))
self.jobs.sort()
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
self.emit(SIGNAL('layoutChanged()'))
def has_device_jobs(self): def has_device_jobs(self):
for job in self.jobs: for job in self.jobs:
if job.is_running and isinstance(job, DeviceJob): if job.is_running and isinstance(job, DeviceJob):

View File

@ -676,21 +676,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
'Select root folder') 'Select root folder')
if not root: if not root:
return return
from calibre.gui2.add import AddRecursive from calibre.gui2.add import Adder
self._add_recursive_thread = AddRecursive(root, self._adder = Adder(self,
self.library_view.model().db, self.get_metadata, self.library_view.model().db,
single, self) Dispatcher(self._files_added))
self.connect(self._add_recursive_thread, SIGNAL('finished()'), self._adder.add_recursive(root, single)
self._recursive_files_added)
self._add_recursive_thread.start()
def _recursive_files_added(self):
self._add_recursive_thread.process_duplicates()
if self._add_recursive_thread.number_of_books_added > 0:
self.library_view.model().resort(reset=False)
self.library_view.model().research()
self.library_view.model().count_changed()
self._add_recursive_thread = None
def add_recursive_single(self, checked): def add_recursive_single(self, checked):
''' '''
@ -731,10 +721,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
(_('LRF Books'), ['lrf']), (_('LRF Books'), ['lrf']),
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']), (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
(_('LIT Books'), ['lit']), (_('LIT Books'), ['lit']),
(_('MOBI Books'), ['mobi', 'prc']), (_('MOBI Books'), ['mobi', 'prc', 'azw']),
(_('Text books'), ['txt', 'rtf']), (_('Text books'), ['txt', 'rtf']),
(_('PDF Books'), ['pdf']), (_('PDF Books'), ['pdf']),
(_('Comics'), ['cbz', 'cbr']), (_('Comics'), ['cbz', 'cbr', 'cbc']),
(_('Archives'), ['zip', 'rar']), (_('Archives'), ['zip', 'rar']),
]) ])
if not books: if not books:
@ -745,40 +735,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def _add_books(self, paths, to_device, on_card=None): def _add_books(self, paths, to_device, on_card=None):
if on_card is None: if on_card is None:
on_card = self.stack.currentIndex() == 2 on_card = self.stack.currentIndex() >= 2
if not paths: if not paths:
return return
from calibre.gui2.add import AddFiles from calibre.gui2.add import Adder
self._add_files_thread = AddFiles(paths, self.default_thumbnail, self._adder = Adder(self,
self.get_metadata, None if to_device else self.library_view.model().db,
None if to_device else \ Dispatcher(partial(self._files_added, on_card=on_card)))
self.library_view.model().db self._adder.add(paths)
)
self._add_files_thread.send_to_device = to_device
self._add_files_thread.on_card = on_card
self._add_files_thread.create_progress_dialog(_('Adding books...'),
_('Reading metadata...'), self)
self.connect(self._add_files_thread, SIGNAL('finished()'),
self._files_added)
self._add_files_thread.start()
def _files_added(self): def _files_added(self, paths=[], names=[], infos=[], on_card=False):
t = self._add_files_thread if paths:
self._add_files_thread = None self.upload_books(paths,
if not t.canceled: list(map(sanitize_file_name, names)),
if t.send_to_device: infos, on_card=on_card)
self.upload_books(t.paths, self.status_bar.showMessage(
list(map(sanitize_file_name, t.names)), _('Uploading books to device.'), 2000)
t.infos, on_card=t.on_card) if self._adder.number_of_books_added > 0:
self.status_bar.showMessage( self.library_view.model().books_added(self._adder.number_of_books_added)
_('Uploading books to device.'), 2000)
else:
t.process_duplicates()
if t.number_of_books_added > 0:
self.library_view.model().books_added(t.number_of_books_added)
if hasattr(self, 'db_images'): if hasattr(self, 'db_images'):
self.db_images.reset() self.db_images.reset()
self._adder = None
############################################################################ ############################################################################
@ -1401,7 +1380,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
except: except:
pass pass
if not self.device_error_dialog.isVisible(): if not self.device_error_dialog.isVisible():
self.device_error_dialog.set_message(job.details) self.device_error_dialog.setDetailedText(job.details)
self.device_error_dialog.show() self.device_error_dialog.show()
def job_exception(self, job): def job_exception(self, job):
@ -1525,8 +1504,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if self.job_manager.has_device_jobs(): if self.job_manager.has_device_jobs():
msg = '<p>'+__appname__ + \ msg = '<p>'+__appname__ + \
_(''' is communicating with the device!<br> _(''' is communicating with the device!<br>
'Quitting may cause corruption on the device.<br> Quitting may cause corruption on the device.<br>
'Are you sure you want to quit?''')+'</p>' Are you sure you want to quit?''')+'</p>'
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg, d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
QMessageBox.Yes|QMessageBox.No, self) QMessageBox.Yes|QMessageBox.No, self)

View File

@ -8,6 +8,7 @@ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre import prints
def option_parser(usage='''\ def option_parser(usage='''\
Usage: %prog [options] Usage: %prog [options]
@ -79,8 +80,8 @@ class MainWindow(QMainWindow):
sio = StringIO.StringIO() sio = StringIO.StringIO()
traceback.print_exception(type, value, tb, file=sio) traceback.print_exception(type, value, tb, file=sio)
fe = sio.getvalue() fe = sio.getvalue()
print >>sys.stderr, fe prints(fe, file=sys.stderr)
msg = unicode(str(value), 'utf8', 'replace') msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe, error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
show=True) show=True)
except: except:

View File

@ -285,6 +285,7 @@ class JobsView(TableView):
job = self.model().row_to_job(row) job = self.model().row_to_job(row)
d = DetailView(self, job) d = DetailView(self, job)
d.exec_() d.exec_()
d.timer.stop()
class FontFamilyModel(QAbstractListModel): class FontFamilyModel(QAbstractListModel):

View File

@ -1183,6 +1183,28 @@ class LibraryDatabase2(LibraryDatabase):
path = path_or_stream path = path_or_stream
return run_plugins_on_import(path, format) return run_plugins_on_import(path, format)
def create_book_entry(self, mi, cover=None, add_duplicates=True):
if not add_duplicates and self.has_book(mi):
return None
series_index = 1 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
if isinstance(title, str):
title = title.decode(preferred_encoding)
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, series_index, aus))
id = obj.lastrowid
self.data.books_added([id], self.conn)
self.set_path(id, True)
self.conn.commit()
self.set_metadata(id, mi)
if cover:
self.set_cover(id, cover)
return id
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
''' '''
Add a book to the database. The result cache is not updated. Add a book to the database. The result cache is not updated.

View File

@ -4,9 +4,10 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, inspect, re import sys, os, inspect, re
from sphinx.builder import StandaloneHTMLBuilder, bold from sphinx.builder import StandaloneHTMLBuilder
from sphinx.util import rpartition from sphinx.util import rpartition
from sphinx.ext.autodoc import get_module_charset, prepare_docstring from sphinx.util.console import bold
from sphinx.ext.autodoc import prepare_docstring
from docutils.statemachine import ViewList from docutils.statemachine import ViewList
from docutils import nodes from docutils import nodes
@ -181,7 +182,7 @@ def auto_member(dirname, arguments, options, content, lineno,
docstring = '\n'.join(comment_lines) docstring = '\n'.join(comment_lines)
if module is not None and docstring is not None: if module is not None and docstring is not None:
docstring = docstring.decode(get_module_charset(mod)) docstring = docstring.decode('utf-8')
result = ViewList() result = ViewList()
result.append('.. attribute:: %s.%s'%(cls.__name__, obj), '<autodoc>') result.append('.. attribute:: %s.%s'%(cls.__name__, obj), '<autodoc>')

View File

@ -17,39 +17,11 @@ E-book Format Conversion
What formats does |app| support conversion to/from? What formats does |app| support conversion to/from?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| supports the conversion of the following formats: |app| supports the conversion of many input formats to many output formats.
It can convert every input format in the following list, to every output format.
+----------------------------+------------------------------------------------------------------+ *Input Formats:* CBZ, CBR, CBC, EPUB, FB2, HTML, LIT, MOBI, ODT, PDF, PRC**, RTF, TXT
| | **Output formats** | *Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PDF, TXT
| +------------------+-----------------------+-----------------------+
| | EPUB | LRF | MOBI |
+===================+========+==================+=======================+=======================+
| | MOBI | ✔ | ✔ | ✔ |
| | | | | |
| | LIT | ✔ | ✔ | ✔ |
| | | | | |
| | PRC** | ✔ | ✔ | ✔ |
| | | | | |
| | EPUB | ✔ | ✔ | ✔ |
| | | | | |
| | ODT | ✔ | ✔ | ✔ |
| | | | | |
| | FB2 | ✔ | ✔ | ✔ |
| | | | | |
| | HTML | ✔ | ✔ | ✔ |
| | | | | |
| **Input formats** | CBR | ✔ | ✔ | ✔ |
| | | | | |
| | CBZ | ✔ | ✔ | ✔ |
| | | | | |
| | RTF | ✔ | ✔ | ✔ |
| | | | | |
| | TXT | ✔ | ✔ | ✔ |
| | | | | |
| | PDF | ✔ | ✔ | ✔ |
| | | | | |
| | LRS | | ✔ | |
+-------------------+--------+------------------+-----------------------+-----------------------+
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers ** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
@ -64,7 +36,7 @@ The PDF conversion tries to extract the text and images from the PDF file and co
are also represented as vector diagrams, thus they cannot be extracted. are also represented as vector diagrams, thus they cannot be extracted.
How do I convert a collection of HTML files in a specific order? How do I convert a collection of HTML files in a specific order?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to convert a collection of HTML files in a specific oder, you have to create a table of contents file. That is, another HTML file that contains links to all the other files in the desired order. Such a file looks like:: In order to convert a collection of HTML files in a specific oder, you have to create a table of contents file. That is, another HTML file that contains links to all the other files in the desired order. Such a file looks like::
<html> <html>

View File

@ -257,38 +257,26 @@ The final new feature is the :meth:`calibre.web.feeds.news.BasicNewsRecipe.prepr
Tips for developing new recipes Tips for developing new recipes
--------------------------------- ---------------------------------
The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.py`. You can download content using this recipe with the command:: The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.recipe`. The `.recipe` extension is required. You can download content using this recipe with the command::
feeds2disk --debug --test myrecipe.py ebook-convert myrecipe.recipe output_dir --test -vv
The :command:`feeds2disk` will download all the webpages and save them to the current directory. The :option:`--debug` makes feeds2disk spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds. The :command:`ebook-convert` will download all the webpages and save them to the directory :file:`output_dir`, creating it if necessary. The :option:`-vv` makes ebook-convert spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds.
Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate an LRF ebook with the command:: Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate ebooks in different formats as shown below::
html2lrf --use-spine --page-break-before "$" index.html ebook-convert myrecipe.recipe myrecipe.epub
ebook-convert myrecipe.recipe myrecipe.mobi
If the generated :term:`LRF` looks good, you can finally, run:: ...
feeds2lrf myrecipe.py
to see the final :term:`LRF` format e-book generated from your recipe. If you're satisfied with your recipe, consider attaching it to `the wiki <http://calibre.kovidgoyal.net/wiki/UserRecipes>`_, so that others can use it as well. If you feel there is enough demand to justify its inclusion into the set of built-in recipes, add a comment to the ticket http://calibre.kovidgoyal.net/ticket/405
If you just want to quickly test a couple of feeds, you can use the :option:`--feeds` option:: If you're satisfied with your recipe, consider attaching it to `the wiki <http://calibre.kovidgoyal.net/wiki/UserRecipes>`_, so that others can use it as well. If you feel there is enough demand to justify its inclusion into the set of built-in recipes, add a comment to the ticket http://calibre.kovidgoyal.net/ticket/405
feeds2disk --feeds "['http://feeds.newsweek.com/newsweek/TopNews', 'http://feeds.newsweek.com/headlines/politics']"
.. seealso:: .. seealso::
:ref:`feeds2disk` :ref:`ebook-convert`
The command line interfce for downloading content from the internet The command line interface for all ebook conversion.
:ref:`feeds2lrf`
The command line interface for downloading content fro the internet and converting it into a :term:`LRF` e-book.
:ref:`html2lrf`
The command line interface for converting :term:`HTML` into a :term:`LRF` e-book.
Further reading Further reading
@ -305,16 +293,6 @@ To learn more about writing advanced recipes using some of the facilities, avail
`Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/head:/src/calibre/web/feeds/recipes/>`_ `Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/head:/src/calibre/web/feeds/recipes/>`_
The source code for the built-in recipes that come with |app| The source code for the built-in recipes that come with |app|
Migrating old style profiles to recipes
----------------------------------------
In earlier versions of |app| there was a similar, if less powerful, framework for fetching news based on *Profiles*. If you have a profile that you would like to migrate to a recipe, the basic technique is simple, as they are very similar (on the surface). Common changes you have to make include:
* Replace ``DefaultProfile`` with ``BasicNewsRecipe``
* Remove ``max_recursions``
* If the server you're downloading from doesn't like multiple connects, set ``simultaneous_downloads = 1``.
API documentation API documentation
-------------------- --------------------

View File

@ -54,7 +54,7 @@ Customizing e-book download
.. automember:: BasicNewsRecipe.timefmt .. automember:: BasicNewsRecipe.timefmt
.. automember:: basicNewsRecipe.conversion_options .. automember:: BasicNewsRecipe.conversion_options
.. automember:: BasicNewsRecipe.feeds .. automember:: BasicNewsRecipe.feeds

View File

@ -42,7 +42,7 @@ class BaseJob(object):
def update(self): def update(self):
if self.duration is not None: if self.duration is not None:
self._run_state = self.FINISHED self._run_state = self.FINISHED
self.percent = 1 self.percent = 100
if self.killed: if self.killed:
self._status_text = _('Stopped') self._status_text = _('Stopped')
else: else:

View File

@ -25,6 +25,9 @@ PARALLEL_FUNCS = {
'gui_convert' : 'gui_convert' :
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'), ('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
'read_metadata' :
('calibre.ebooks.metadata.worker', 'read_metadata_', 'notification'),
} }
class Progress(Thread): class Progress(Thread):