mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Various minor fixes. WARNING: Adding of books is currently broken.
This commit is contained in:
parent
2e0ad5d1e0
commit
1a117fd070
@ -9,43 +9,43 @@ a backend that implement the Device interface for the SONY PRS500 Reader.
|
||||
from calibre.customize import Plugin
|
||||
|
||||
class DevicePlugin(Plugin):
|
||||
"""
|
||||
Defines the interface that should be implemented by backends that
|
||||
communicate with an ebook reader.
|
||||
|
||||
"""
|
||||
Defines the interface that should be implemented by backends that
|
||||
communicate with an ebook reader.
|
||||
|
||||
The C{end_session} variables are used for USB session management. Sometimes
|
||||
the front-end needs to call several methods one after another, in which case
|
||||
the front-end needs to call several methods one after another, in which case
|
||||
the USB session should not be closed after each method call.
|
||||
"""
|
||||
type = _('Device Interface')
|
||||
|
||||
|
||||
# Ordered list of supported formats
|
||||
FORMATS = ["lrf", "rtf", "pdf", "txt"]
|
||||
VENDOR_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.
|
||||
BCD = None
|
||||
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
|
||||
# Whether the metadata on books can be set via the GUI.
|
||||
CAN_SET_METADATA = True
|
||||
|
||||
|
||||
def reset(self, key='-1', log_packets=False, report_progress=None) :
|
||||
"""
|
||||
@param key: The key to unlock the device
|
||||
@param log_packets: If true the packet stream to/from the device is logged
|
||||
@param report_progress: Function that is called with a % progress
|
||||
@param log_packets: If true the packet stream to/from the device is logged
|
||||
@param report_progress: Function that is called with a % progress
|
||||
(number between 0 and 100) for various tasks
|
||||
If it is called with -1 that means that the
|
||||
If it is called with -1 that means that the
|
||||
task does not have any progress information
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_fdi(cls):
|
||||
'''Return the FDI description of this device for HAL on linux.'''
|
||||
return ''
|
||||
|
||||
|
||||
@classmethod
|
||||
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
|
||||
it can do some relatively time intensive checks. The default implementation
|
||||
returns True.
|
||||
|
||||
:param device_info: On windows a device ID string. On Unix a tuple of
|
||||
``(vendor_id, product_id, bcd)``.
|
||||
|
||||
:param device_info: On windows a device ID string. On Unix a tuple of
|
||||
``(vendor_id, product_id, bcd)``.
|
||||
'''
|
||||
return True
|
||||
|
||||
|
||||
def open(self):
|
||||
'''
|
||||
Perform any device specific initialization. Called after the device is
|
||||
detected but before any other functions that communicate with the device.
|
||||
For example: For devices that present themselves as USB Mass storage
|
||||
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
|
||||
that should serve as a good example for USB Mass storage devices.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def card_prefix(self, end_session=True):
|
||||
'''
|
||||
Return a 2 element list of the prefix to paths on the cards.
|
||||
@ -99,9 +99,9 @@ class DevicePlugin(Plugin):
|
||||
(None, None)
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def total_space(self, end_session=True):
|
||||
"""
|
||||
"""
|
||||
Get total space available on the mountpoints:
|
||||
1. Main memory
|
||||
2. Memory Card A
|
||||
@ -111,9 +111,9 @@ class DevicePlugin(Plugin):
|
||||
particular device doesn't have any of these locations it should return 0.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def free_space(self, end_session=True):
|
||||
"""
|
||||
"""
|
||||
Get free space available on the mountpoints:
|
||||
1. Main memory
|
||||
2. 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
|
||||
particular device doesn't have any of these locations it should return -1.
|
||||
"""
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def books(self, oncard=None, end_session=True):
|
||||
"""
|
||||
"""
|
||||
Return a list of ebooks on the device.
|
||||
@param oncard: If 'carda' or 'cardb' return a list of ebooks on the
|
||||
specific storage card, otherwise return list of ebooks
|
||||
in main memory of device. If a card is specified and no
|
||||
books are on the card return empty list.
|
||||
@return: A BookList.
|
||||
"""
|
||||
books are on the card return empty list.
|
||||
@return: A BookList.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def upload_books(self, files, names, on_card=None, end_session=True,
|
||||
metadata=None):
|
||||
'''
|
||||
@ -144,26 +144,26 @@ class DevicePlugin(Plugin):
|
||||
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".
|
||||
@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)
|
||||
@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}.
|
||||
@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
|
||||
based on tags. len(metadata) == len(files). If your device does not support
|
||||
hierarchical ebook folders, you can safely ignore this parameter.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@classmethod
|
||||
def add_books_to_metadata(cls, locations, metadata, booklists):
|
||||
'''
|
||||
Add locations to the booklists. This function must not communicate with
|
||||
the device.
|
||||
Add locations to the booklists. This function must not communicate with
|
||||
the device.
|
||||
@param locations: Result of a call to L{upload_books}
|
||||
@param metadata: List of dictionaries. Each dictionary must have the
|
||||
keys C{title}, C{authors}, C{author_sort}, C{cover}, C{tags}.
|
||||
The value of the C{cover}
|
||||
keys C{title}, C{authors}, C{author_sort}, C{cover}, C{tags}.
|
||||
The value of the C{cover}
|
||||
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
|
||||
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
|
||||
another dictionary that maps tag names to lists of book ids. The ids are
|
||||
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='cardb')).
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def delete_books(self, paths, end_session=True):
|
||||
'''
|
||||
Delete books at paths on device.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@classmethod
|
||||
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.
|
||||
@param paths: paths to books on the device.
|
||||
@param booklists: A tuple containing the result of calls to
|
||||
@ -195,7 +195,7 @@ class DevicePlugin(Plugin):
|
||||
L{books}(oncard='cardb')).
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def sync_booklists(self, booklists, end_session=True):
|
||||
'''
|
||||
Update metadata on device.
|
||||
@ -204,8 +204,8 @@ class DevicePlugin(Plugin):
|
||||
L{books}(oncard='cardb')).
|
||||
'''
|
||||
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.
|
||||
@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):
|
||||
'''
|
||||
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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BookList(list):
|
||||
'''
|
||||
A list of books. Each Book object must have the fields:
|
||||
@ -247,21 +247,21 @@ class BookList(list):
|
||||
4. datetime (a UTC time tuple)
|
||||
5. path (path on the device to the book)
|
||||
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
|
||||
__setslice__ = None
|
||||
|
||||
|
||||
def supports_tags(self):
|
||||
''' Return True if the the device supports tags (collections) for this book list. '''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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 book: A book object that is in this BookList.
|
||||
@param book: A book object that is in this BookList.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -53,7 +53,7 @@ class KINDLE(USBMS):
|
||||
|
||||
@classmethod
|
||||
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])
|
||||
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))
|
||||
|
@ -1,8 +1,8 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
"""
|
||||
This module contains the logic for dealing with XML book lists found
|
||||
in the reader cache.
|
||||
"""
|
||||
This module contains the logic for dealing with XML book lists found
|
||||
in the reader cache.
|
||||
"""
|
||||
import xml.dom.minidom as dom
|
||||
from base64 import b64decode as decode
|
||||
@ -25,16 +25,16 @@ def sortable_title(title):
|
||||
|
||||
class book_metadata_field(object):
|
||||
""" Represents metadata stored as an attribute """
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
self.formatter = formatter
|
||||
self.setter = setter
|
||||
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
""" Return a string. String may be empty if self.attr is absent """
|
||||
return self.formatter(obj.elem.getAttribute(self.attr)) if \
|
||||
self.formatter else obj.elem.getAttribute(self.attr).strip()
|
||||
|
||||
|
||||
def __set__(self, obj, val):
|
||||
""" Set the attribute """
|
||||
val = self.setter(val) if self.setter else val
|
||||
@ -44,7 +44,7 @@ class book_metadata_field(object):
|
||||
|
||||
class Book(object):
|
||||
""" Provides a view onto the XML element that represents a book """
|
||||
|
||||
|
||||
title = book_metadata_field("title")
|
||||
authors = book_metadata_field("author", \
|
||||
formatter=lambda x: x if x and x.strip() else "Unknown")
|
||||
@ -66,12 +66,12 @@ class Book(object):
|
||||
def fset(self, val):
|
||||
self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
|
||||
return property(doc=doc, fget=fget, fset=fset)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def thumbnail(self):
|
||||
doc = \
|
||||
"""
|
||||
The thumbnail. Should be a height 68 image.
|
||||
"""
|
||||
The thumbnail. Should be a height 68 image.
|
||||
Setting is not supported.
|
||||
"""
|
||||
def fget(self):
|
||||
@ -83,18 +83,18 @@ class Book(object):
|
||||
break
|
||||
rc = ""
|
||||
for node in th.childNodes:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc += node.data
|
||||
return decode(rc)
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def path(self):
|
||||
doc = """ Absolute path to book on device. Setting not supported. """
|
||||
def fget(self):
|
||||
def fget(self):
|
||||
return self.root + self.rpath
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def db_id(self):
|
||||
doc = '''The database id in the application database that this file corresponds to'''
|
||||
@ -103,20 +103,20 @@ class Book(object):
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
def __init__(self, node, tags=[], prefix="", root="/Data/media/"):
|
||||
self.elem = node
|
||||
self.prefix = prefix
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
|
||||
|
||||
def __str__(self):
|
||||
""" Return a utf-8 encoded string with title author and path information """
|
||||
return self.title.encode('utf-8') + " by " + \
|
||||
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
|
||||
|
||||
|
||||
def fix_ids(media, cache):
|
||||
def fix_ids(media, cache, *args):
|
||||
'''
|
||||
Adjust ids in cache to correspond with media.
|
||||
'''
|
||||
@ -131,16 +131,16 @@ def fix_ids(media, cache):
|
||||
child.setAttribute("id", str(cid))
|
||||
cid += 1
|
||||
media.set_next_id(str(cid))
|
||||
|
||||
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
__getslice__ = None
|
||||
__setslice__ = None
|
||||
|
||||
|
||||
def __init__(self, root="/Data/media/", sfile=None):
|
||||
_BookList.__init__(self)
|
||||
self.tag_order = {}
|
||||
@ -163,25 +163,25 @@ class BookList(_BookList):
|
||||
if records:
|
||||
self.prefix = 'xs1:'
|
||||
self.root = records[0]
|
||||
self.proot = root
|
||||
|
||||
self.proot = root
|
||||
|
||||
for book in self.document.getElementsByTagName(self.prefix + "text"):
|
||||
id = book.getAttribute('id')
|
||||
pl = [i.getAttribute('title') for i in self.get_playlists(id)]
|
||||
self.append(Book(book, root=root, prefix=self.prefix, tags=pl))
|
||||
|
||||
|
||||
def supports_tags(self):
|
||||
return bool(self.prefix)
|
||||
|
||||
|
||||
def playlists(self):
|
||||
return self.root.getElementsByTagName(self.prefix+'playlist')
|
||||
|
||||
def playlist_items(self):
|
||||
|
||||
def playlist_items(self):
|
||||
plitems = []
|
||||
for pl in self.playlists():
|
||||
plitems.extend(pl.getElementsByTagName(self.prefix+'item'))
|
||||
return plitems
|
||||
|
||||
|
||||
def purge_corrupted_files(self):
|
||||
if not self.root:
|
||||
return []
|
||||
@ -193,32 +193,32 @@ class BookList(_BookList):
|
||||
c.parentNode.removeChild(c)
|
||||
c.unlink()
|
||||
return paths
|
||||
|
||||
|
||||
def purge_empty_playlists(self):
|
||||
''' Remove all playlist entries that have no children. '''
|
||||
for pl in self.playlists():
|
||||
if not pl.getElementsByTagName(self.prefix + 'item'):
|
||||
pl.parentNode.removeChild(pl)
|
||||
pl.unlink()
|
||||
|
||||
|
||||
def _delete_book(self, node):
|
||||
nid = node.getAttribute('id')
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
self.remove_from_playlists(nid)
|
||||
|
||||
|
||||
|
||||
|
||||
def delete_book(self, cid):
|
||||
'''
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{id == cid}.
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for book in self:
|
||||
if str(book.id) == str(cid):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
|
||||
def remove_book(self, path):
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{path == path}.
|
||||
@ -227,15 +227,15 @@ class BookList(_BookList):
|
||||
for book in self:
|
||||
if path.endswith(book.rpath):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
|
||||
def next_id(self):
|
||||
return self.document.documentElement.getAttribute('nextID')
|
||||
|
||||
|
||||
def set_next_id(self, id):
|
||||
self.document.documentElement.setAttribute('nextID', str(id))
|
||||
|
||||
|
||||
def max_id(self):
|
||||
max = 0
|
||||
for child in self.root.childNodes:
|
||||
@ -243,15 +243,15 @@ class BookList(_BookList):
|
||||
nid = int(child.getAttribute('id'))
|
||||
if nid > max:
|
||||
max = nid
|
||||
return max
|
||||
|
||||
return max
|
||||
|
||||
def book_by_path(self, path):
|
||||
for child in self.root.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
|
||||
if path == child.getAttribute('path'):
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def add_book(self, info, name, size, ctime):
|
||||
""" Add a node into DOM tree representing a book """
|
||||
book = self.book_by_path(name)
|
||||
@ -262,23 +262,23 @@ class BookList(_BookList):
|
||||
cid = self.max_id()+1
|
||||
sourceid = str(self[0].sourceid) if len(self) else "1"
|
||||
attrs = {
|
||||
"title" : info["title"],
|
||||
"title" : info["title"],
|
||||
'titleSorter' : sortable_title(info['title']),
|
||||
"author" : info["authors"] if info['authors'] else 'Unknown', \
|
||||
"page":"0", "part":"0", "scale":"0", \
|
||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||
"mime":mime, "path":name, "size":str(size)
|
||||
}
|
||||
}
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
try:
|
||||
w, h, data = info["cover"]
|
||||
w, h, data = info["cover"]
|
||||
except TypeError:
|
||||
w, h, data = None, None, None
|
||||
|
||||
|
||||
if data:
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th.setAttribute("width", str(w))
|
||||
th.setAttribute("height", str(h))
|
||||
jpeg = self.document.createElement(self.prefix + "jpeg")
|
||||
@ -294,15 +294,15 @@ class BookList(_BookList):
|
||||
if info.has_key('tag order'):
|
||||
self.tag_order.update(info['tag order'])
|
||||
self.set_playlists(book.id, info['tags'])
|
||||
|
||||
|
||||
|
||||
|
||||
def playlist_by_title(self, title):
|
||||
for pl in self.playlists():
|
||||
if pl.getAttribute('title').lower() == title.lower():
|
||||
return pl
|
||||
|
||||
|
||||
def add_playlist(self, title):
|
||||
cid = self.max_id()+1
|
||||
cid = self.max_id()+1
|
||||
pl = self.document.createElement(self.prefix+'playlist')
|
||||
pl.setAttribute('sourceid', '0')
|
||||
pl.setAttribute('id', str(cid))
|
||||
@ -316,18 +316,18 @@ class BookList(_BookList):
|
||||
except AttributeError:
|
||||
continue
|
||||
return pl
|
||||
|
||||
|
||||
|
||||
|
||||
def remove_from_playlists(self, id):
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == str(id):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
book.tags = tags
|
||||
self.set_playlists(book.id, tags)
|
||||
|
||||
|
||||
def set_playlists(self, id, collections):
|
||||
self.remove_from_playlists(id)
|
||||
for collection in set(collections):
|
||||
@ -337,7 +337,7 @@ class BookList(_BookList):
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(id))
|
||||
coll.appendChild(item)
|
||||
|
||||
|
||||
def get_playlists(self, id):
|
||||
ans = []
|
||||
for pl in self.playlists():
|
||||
@ -346,12 +346,12 @@ class BookList(_BookList):
|
||||
ans.append(pl)
|
||||
continue
|
||||
return ans
|
||||
|
||||
|
||||
def book_by_id(self, id):
|
||||
for book in self:
|
||||
if str(book.id) == str(id):
|
||||
return book
|
||||
|
||||
|
||||
def reorder_playlists(self):
|
||||
for title in self.tag_order.keys():
|
||||
pl = self.playlist_by_title(title)
|
||||
@ -364,7 +364,7 @@ class BookList(_BookList):
|
||||
map[i] = j
|
||||
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]
|
||||
|
||||
|
||||
if len(ordered_ids) < len(pl.childNodes):
|
||||
continue
|
||||
children = [i for i in pl.childNodes if hasattr(i, 'getAttribute')]
|
||||
@ -374,8 +374,8 @@ class BookList(_BookList):
|
||||
for id in ordered_ids:
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(map[id]))
|
||||
pl.appendChild(item)
|
||||
|
||||
pl.appendChild(item)
|
||||
|
||||
def write(self, stream):
|
||||
""" Write XML representation of DOM tree to C{stream} """
|
||||
stream.write(self.document.toxml('utf-8'))
|
||||
stream.write(self.document.toxml('utf-8'))
|
||||
|
@ -47,6 +47,7 @@ from calibre.devices.prs500.prstypes import *
|
||||
from calibre.devices.errors import *
|
||||
from calibre.devices.prs500.books import BookList, fix_ids
|
||||
from calibre import __author__, __appname__
|
||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
||||
|
||||
# Protocol versions this driver has been tested with
|
||||
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
|
||||
@ -76,7 +77,7 @@ class File(object):
|
||||
return self.name
|
||||
|
||||
|
||||
class PRS500(DevicePlugin):
|
||||
class PRS500(DeviceConfig, DevicePlugin):
|
||||
|
||||
"""
|
||||
Implements the backend for communication with the SONY Reader.
|
||||
@ -624,6 +625,8 @@ class PRS500(DevicePlugin):
|
||||
data_type=FreeSpaceAnswer, \
|
||||
command_number=FreeSpaceQuery.NUMBER)[0]
|
||||
data.append( pkt.free )
|
||||
data = [x for x in data if x != 0]
|
||||
data.append(0)
|
||||
return data
|
||||
|
||||
def _exists(self, path):
|
||||
|
@ -15,11 +15,11 @@ from calibre.devices import strptime
|
||||
|
||||
strftime = functools.partial(_strftime, zone=time.gmtime)
|
||||
|
||||
MIME_MAP = {
|
||||
MIME_MAP = {
|
||||
"lrf" : "application/x-sony-bbeb",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
"rtf" : "application/rtf",
|
||||
"pdf" : "application/pdf",
|
||||
'lrx' : 'application/x-sony-bbeb',
|
||||
"rtf" : "application/rtf",
|
||||
"pdf" : "application/pdf",
|
||||
"txt" : "text/plain" ,
|
||||
'epub': 'application/epub+zip',
|
||||
}
|
||||
@ -32,16 +32,16 @@ def sortable_title(title):
|
||||
|
||||
class book_metadata_field(object):
|
||||
""" Represents metadata stored as an attribute """
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
def __init__(self, attr, formatter=None, setter=None):
|
||||
self.attr = attr
|
||||
self.formatter = formatter
|
||||
self.setter = setter
|
||||
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
""" Return a string. String may be empty if self.attr is absent """
|
||||
return self.formatter(obj.elem.getAttribute(self.attr)) if \
|
||||
self.formatter else obj.elem.getAttribute(self.attr).strip()
|
||||
|
||||
|
||||
def __set__(self, obj, val):
|
||||
""" Set the attribute """
|
||||
val = self.setter(val) if self.setter else val
|
||||
@ -52,7 +52,7 @@ class book_metadata_field(object):
|
||||
|
||||
class Book(object):
|
||||
""" Provides a view onto the XML element that represents a book """
|
||||
|
||||
|
||||
title = book_metadata_field("title")
|
||||
authors = book_metadata_field("author", \
|
||||
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)))
|
||||
# When setting this attribute you must use an epoch
|
||||
datetime = book_metadata_field("date", formatter=strptime, setter=strftime)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def title_sorter(self):
|
||||
doc = '''String to sort the title. If absent, title is returned'''
|
||||
@ -75,12 +75,12 @@ class Book(object):
|
||||
def fset(self, val):
|
||||
self.elem.setAttribute('titleSorter', sortable_title(unicode(val)))
|
||||
return property(doc=doc, fget=fget, fset=fset)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def thumbnail(self):
|
||||
doc = \
|
||||
"""
|
||||
The thumbnail. Should be a height 68 image.
|
||||
"""
|
||||
The thumbnail. Should be a height 68 image.
|
||||
Setting is not supported.
|
||||
"""
|
||||
def fget(self):
|
||||
@ -94,18 +94,18 @@ class Book(object):
|
||||
break
|
||||
rc = ""
|
||||
for node in th.childNodes:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc += node.data
|
||||
return decode(rc)
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def path(self):
|
||||
doc = """ Absolute path to book on device. Setting not supported. """
|
||||
def fget(self):
|
||||
def fget(self):
|
||||
return self.mountpath + self.rpath
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
@dynamic_property
|
||||
def db_id(self):
|
||||
doc = '''The database id in the application database that this file corresponds to'''
|
||||
@ -114,13 +114,13 @@ class Book(object):
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return property(fget=fget, doc=doc)
|
||||
|
||||
|
||||
def __init__(self, node, mountpath, tags, prefix=""):
|
||||
self.elem = node
|
||||
self.prefix = prefix
|
||||
self.tags = tags
|
||||
self.mountpath = mountpath
|
||||
|
||||
|
||||
def __str__(self):
|
||||
""" Return a utf-8 encoded string with title author and path information """
|
||||
return self.title.encode('utf-8') + " by " + \
|
||||
@ -128,7 +128,7 @@ class Book(object):
|
||||
|
||||
|
||||
class BookList(_BookList):
|
||||
|
||||
|
||||
def __init__(self, xml_file, mountpath, report_progress=None):
|
||||
_BookList.__init__(self)
|
||||
xml_file.seek(0)
|
||||
@ -143,15 +143,15 @@ class BookList(_BookList):
|
||||
self.root_element = records[0]
|
||||
else:
|
||||
self.prefix = ''
|
||||
|
||||
|
||||
nodes = self.root_element.childNodes
|
||||
for i, book in enumerate(nodes):
|
||||
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'):
|
||||
tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))]
|
||||
self.append(Book(book, mountpath, tags, prefix=self.prefix))
|
||||
|
||||
|
||||
def max_id(self):
|
||||
max = 0
|
||||
for child in self.root_element.childNodes:
|
||||
@ -160,7 +160,7 @@ class BookList(_BookList):
|
||||
if nid > max:
|
||||
max = nid
|
||||
return max
|
||||
|
||||
|
||||
def is_id_valid(self, id):
|
||||
'''Return True iff there is an element with C{id==id}.'''
|
||||
id = str(id)
|
||||
@ -169,23 +169,23 @@ class BookList(_BookList):
|
||||
if child.getAttribute('id') == id:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def supports_tags(self):
|
||||
return True
|
||||
|
||||
|
||||
def book_by_path(self, path):
|
||||
for child in self.root_element.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and child.hasAttribute("path"):
|
||||
if path == child.getAttribute('path'):
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def add_book(self, info, name, size, ctime):
|
||||
""" Add a node into the DOM tree, representing a book """
|
||||
book = self.book_by_path(name)
|
||||
if book is not None:
|
||||
self.remove_book(name)
|
||||
|
||||
|
||||
node = self.document.createElement(self.prefix + "text")
|
||||
mime = MIME_MAP[name.rpartition('.')[-1].lower()]
|
||||
cid = self.max_id()+1
|
||||
@ -194,23 +194,23 @@ class BookList(_BookList):
|
||||
except:
|
||||
sourceid = '1'
|
||||
attrs = {
|
||||
"title" : info["title"],
|
||||
"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", \
|
||||
"sourceid":sourceid, "id":str(cid), "date":"", \
|
||||
"mime":mime, "path":name, "size":str(size)
|
||||
}
|
||||
}
|
||||
for attr in attrs.keys():
|
||||
node.setAttributeNode(self.document.createAttribute(attr))
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
node.setAttribute(attr, attrs[attr])
|
||||
try:
|
||||
w, h, data = info["cover"]
|
||||
w, h, data = info["cover"]
|
||||
except TypeError:
|
||||
w, h, data = None, None, None
|
||||
|
||||
|
||||
if data:
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th = self.document.createElement(self.prefix + "thumbnail")
|
||||
th.setAttribute("width", str(w))
|
||||
th.setAttribute("height", str(h))
|
||||
jpeg = self.document.createElement(self.prefix + "jpeg")
|
||||
@ -225,24 +225,24 @@ class BookList(_BookList):
|
||||
if info.has_key('tag order'):
|
||||
self.tag_order.update(info['tag order'])
|
||||
self.set_tags(book, info['tags'])
|
||||
|
||||
|
||||
def _delete_book(self, node):
|
||||
nid = node.getAttribute('id')
|
||||
self.remove_from_playlists(nid)
|
||||
node.parentNode.removeChild(node)
|
||||
node.unlink()
|
||||
|
||||
|
||||
def delete_book(self, cid):
|
||||
'''
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{id == cid}.
|
||||
Also remove book from any collections it is part of.
|
||||
'''
|
||||
for book in self:
|
||||
if str(book.id) == str(cid):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
|
||||
def remove_book(self, path):
|
||||
'''
|
||||
Remove DOM node corresponding to book with C{path == path}.
|
||||
@ -251,24 +251,24 @@ class BookList(_BookList):
|
||||
for book in self:
|
||||
if path.endswith(book.rpath):
|
||||
self.remove(book)
|
||||
self._delete_book(book.elem)
|
||||
self._delete_book(book.elem)
|
||||
break
|
||||
|
||||
|
||||
def playlists(self):
|
||||
ans = []
|
||||
for c in self.root_element.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('playlist'):
|
||||
ans.append(c)
|
||||
return ans
|
||||
|
||||
def playlist_items(self):
|
||||
|
||||
def playlist_items(self):
|
||||
plitems = []
|
||||
for pl in self.playlists():
|
||||
for c in pl.childNodes:
|
||||
if hasattr(c, 'tagName') and c.tagName.endswith('item'):
|
||||
plitems.append(c)
|
||||
return plitems
|
||||
|
||||
|
||||
def purge_corrupted_files(self):
|
||||
if not self.root_element:
|
||||
return []
|
||||
@ -279,7 +279,7 @@ class BookList(_BookList):
|
||||
c.parentNode.removeChild(c)
|
||||
c.unlink()
|
||||
return paths
|
||||
|
||||
|
||||
def purge_empty_playlists(self):
|
||||
''' Remove all playlists that have no children. Also removes any invalid playlist items.'''
|
||||
for pli in self.playlist_items():
|
||||
@ -298,32 +298,32 @@ class BookList(_BookList):
|
||||
if empty:
|
||||
pl.parentNode.removeChild(pl)
|
||||
pl.unlink()
|
||||
|
||||
|
||||
def playlist_by_title(self, title):
|
||||
for pl in self.playlists():
|
||||
if pl.getAttribute('title').lower() == title.lower():
|
||||
return pl
|
||||
|
||||
|
||||
def add_playlist(self, title):
|
||||
cid = self.max_id()+1
|
||||
cid = self.max_id()+1
|
||||
pl = self.document.createElement(self.prefix+'playlist')
|
||||
pl.setAttribute('id', str(cid))
|
||||
pl.setAttribute('title', title)
|
||||
pl.setAttribute('uuid', uuid())
|
||||
self.root_element.insertBefore(pl, self.root_element.childNodes[-1])
|
||||
return pl
|
||||
|
||||
|
||||
def remove_from_playlists(self, id):
|
||||
for pli in self.playlist_items():
|
||||
if pli.getAttribute('id') == str(id):
|
||||
pli.parentNode.removeChild(pli)
|
||||
pli.unlink()
|
||||
|
||||
|
||||
def set_tags(self, book, tags):
|
||||
tags = [t for t in tags if t]
|
||||
book.tags = tags
|
||||
self.set_playlists(book.id, tags)
|
||||
|
||||
|
||||
def set_playlists(self, id, collections):
|
||||
self.remove_from_playlists(id)
|
||||
for collection in set(collections):
|
||||
@ -333,7 +333,7 @@ class BookList(_BookList):
|
||||
item = self.document.createElement(self.prefix+'item')
|
||||
item.setAttribute('id', str(id))
|
||||
coll.appendChild(item)
|
||||
|
||||
|
||||
def get_playlists(self, bookid):
|
||||
ans = []
|
||||
for pl in self.playlists():
|
||||
@ -342,23 +342,23 @@ class BookList(_BookList):
|
||||
if item.getAttribute('id') == str(bookid):
|
||||
ans.append(pl)
|
||||
return ans
|
||||
|
||||
|
||||
def next_id(self):
|
||||
return self.document.documentElement.getAttribute('nextID')
|
||||
|
||||
|
||||
def set_next_id(self, id):
|
||||
self.document.documentElement.setAttribute('nextID', str(id))
|
||||
|
||||
|
||||
def write(self, stream):
|
||||
""" Write XML representation of DOM tree to C{stream} """
|
||||
src = self.document.toxml('utf-8') + '\n'
|
||||
stream.write(src.replace("'", '''))
|
||||
|
||||
|
||||
def book_by_id(self, id):
|
||||
for book in self:
|
||||
if str(book.id) == str(id):
|
||||
return book
|
||||
|
||||
|
||||
def reorder_playlists(self):
|
||||
for title in self.tag_order.keys():
|
||||
pl = self.playlist_by_title(title)
|
||||
@ -371,7 +371,7 @@ class BookList(_BookList):
|
||||
map[i] = j
|
||||
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]
|
||||
|
||||
|
||||
if len(ordered_ids) < len(pl.childNodes):
|
||||
continue
|
||||
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.setAttribute('id', str(map[id]))
|
||||
pl.appendChild(item)
|
||||
|
||||
|
||||
def fix_ids(main, carda, cardb):
|
||||
'''
|
||||
Adjust ids the XML databases.
|
||||
@ -393,7 +393,7 @@ def fix_ids(main, carda, cardb):
|
||||
carda.purge_empty_playlists()
|
||||
if hasattr(cardb, 'purge_empty_playlists'):
|
||||
cardb.purge_empty_playlists()
|
||||
|
||||
|
||||
def regen_ids(db):
|
||||
if not hasattr(db, 'root_element'):
|
||||
return
|
||||
@ -402,11 +402,11 @@ def fix_ids(main, carda, cardb):
|
||||
cid = 0 if db == main else 1
|
||||
for child in db.root_element.childNodes:
|
||||
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('id', str(cid))
|
||||
child.setAttribute('id', str(cid))
|
||||
cid += 1
|
||||
|
||||
|
||||
for item in db.playlist_items():
|
||||
oid = item.getAttribute('id')
|
||||
try:
|
||||
@ -414,11 +414,11 @@ def fix_ids(main, carda, cardb):
|
||||
except KeyError:
|
||||
item.parentNode.removeChild(item)
|
||||
item.unlink()
|
||||
|
||||
|
||||
db.reorder_playlists()
|
||||
|
||||
|
||||
regen_ids(main)
|
||||
regen_ids(carda)
|
||||
regen_ids(cardb)
|
||||
|
||||
|
||||
main.set_next_id(str(main.max_id()+1))
|
||||
|
@ -12,7 +12,8 @@ class DeviceConfig(object):
|
||||
|
||||
@classmethod
|
||||
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)
|
||||
return c
|
||||
|
||||
@ -33,7 +34,7 @@ class DeviceConfig(object):
|
||||
@classmethod
|
||||
def settings(cls):
|
||||
return cls._config().parse()
|
||||
|
||||
|
||||
def customization_help(cls, gui=False):
|
||||
return cls.HELP_MESSAGE
|
||||
|
||||
|
96
src/calibre/ebooks/conversion/config.py
Normal file
96
src/calibre/ebooks/conversion/config.py
Normal 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
|
||||
|
||||
|
@ -241,8 +241,13 @@ class MetaInformation(object):
|
||||
self.tags += mi.tags
|
||||
self.tags = list(set(self.tags))
|
||||
|
||||
if getattr(mi, 'cover_data', None) and mi.cover_data[0] is not None:
|
||||
self.cover_data = mi.cover_data
|
||||
if getattr(mi, 'cover_data', False):
|
||||
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', '')
|
||||
other_comments = getattr(mi, 'comments', '')
|
||||
|
@ -9,12 +9,9 @@ __copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net and ' \
|
||||
'Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
import os
|
||||
from struct import pack, unpack
|
||||
from cStringIO import StringIO
|
||||
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.langcodes import iana2mobi
|
||||
|
||||
@ -116,8 +113,13 @@ class MetadataUpdater(object):
|
||||
|
||||
def update(self, mi):
|
||||
recs = []
|
||||
from calibre.ebooks.mobi.from_any import config
|
||||
if mi.author_sort and config().parse().prefer_author_sort:
|
||||
try:
|
||||
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
|
||||
recs.append((100, authors.encode(self.codec, 'replace')))
|
||||
elif mi.authors:
|
||||
|
111
src/calibre/ebooks/metadata/worker.py
Normal file
111
src/calibre/ebooks/metadata/worker.py
Normal 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
|
@ -2,238 +2,152 @@
|
||||
UI for adding books to the database
|
||||
'''
|
||||
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.constants import preferred_encoding
|
||||
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):
|
||||
|
||||
def __init__(self):
|
||||
QThread.__init__(self)
|
||||
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
|
||||
class RecursiveFind(QThread):
|
||||
|
||||
def __init__(self, parent, db, root, single):
|
||||
QThread.__init__(self, parent)
|
||||
self.db = db
|
||||
self.formats, self.metadata, self.names, self.infos = [], [], [], []
|
||||
self.duplicates = []
|
||||
self.path = root, self.single_book_per_directory = single
|
||||
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.connect(self.get_metadata,
|
||||
SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.metadata_delivered)
|
||||
|
||||
def metadata_delivered(self, id, mi):
|
||||
if self.is_canceled():
|
||||
self.wake_up()
|
||||
self.rfind = self.worker = self.timer = None
|
||||
self.callback = callback
|
||||
self.infos, self.paths, self.names = [], [], []
|
||||
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
|
||||
|
||||
def add_recursive(self, root, single=True):
|
||||
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
|
||||
|
||||
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:
|
||||
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.decode(preferred_encoding, 'replace')
|
||||
self.metadata.append(mi)
|
||||
self.infos.append({'title':mi.title,
|
||||
'authors':', '.join(mi.authors),
|
||||
'cover':self.default_thumbnail, 'tags':[]})
|
||||
self.pd.set_msg(_('Added')+' '+mi.title)
|
||||
|
||||
if self.db is not None:
|
||||
duplicates, num = self.db.add_books(self.paths[id:id+1],
|
||||
self.formats[id:id+1], [mi],
|
||||
add_duplicates=False)
|
||||
self.number_of_books_added += num
|
||||
if duplicates:
|
||||
if not self.duplicates:
|
||||
self.duplicates = [[], [], [], []]
|
||||
for i in range(4):
|
||||
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)
|
||||
if cover:
|
||||
cover = open(cover, 'rb').read()
|
||||
id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
|
||||
self.number_of_books_added += 1
|
||||
if id is None:
|
||||
self.duplicates.append((mi, cover, formats))
|
||||
else:
|
||||
self.add_formats(id, formats)
|
||||
else:
|
||||
self.pd.set_msg(_('Read metadata from ')+title)
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.canceled = False
|
||||
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
|
||||
|
||||
|
||||
self.names.append(name)
|
||||
self.paths.append(formats[0])
|
||||
self.infos.append({'title':mi.title,
|
||||
'authors':', '.join(mi.authors),
|
||||
'cover':None,
|
||||
'tags':mi.tags if mi.tags else []})
|
||||
|
||||
def process_duplicates(self):
|
||||
if self.duplicates:
|
||||
files = ''
|
||||
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!'),
|
||||
files = [x[0].title for x in self.duplicates]
|
||||
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:
|
||||
for mi, formats in self.duplicates:
|
||||
self.db.import_book(mi, formats, notify=False)
|
||||
self.number_of_books_added += 1
|
||||
|
||||
|
||||
'exist in the database. Add them anyway?'),
|
||||
'\n'.join(files), parent=self._parent)
|
||||
if d.exec_() == d.Accepted:
|
||||
for mi, cover, formats in self.duplicates:
|
||||
id = self.db.create_book_entry(mi, cover=cover,
|
||||
add_duplicates=False)
|
||||
self.add_formats(id, formats)
|
||||
self.number_of_books_added += 1
|
||||
|
||||
|
@ -6,96 +6,13 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
|
||||
QCheckBox, QComboBox, Qt, QIcon, SIGNAL
|
||||
|
||||
from calibre.customize.conversion import OptionRecommendation
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.lock import ExclusiveFile
|
||||
from calibre import sanitize_file_name
|
||||
|
||||
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
|
||||
|
||||
from calibre.ebooks.conversion.config import load_defaults, \
|
||||
save_defaults as save_defaults_, \
|
||||
load_specifics, GuiRecommendations
|
||||
|
||||
class Widget(QWidget):
|
||||
|
||||
|
@ -11,7 +11,7 @@ import sys, cPickle
|
||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
||||
|
||||
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
|
||||
from calibre.gui2.convert.single_ui import Ui_Dialog
|
||||
from calibre.gui2.convert.metadata import MetadataWidget
|
||||
|
@ -34,18 +34,18 @@ class DeviceJob(BaseJob):
|
||||
BaseJob.__init__(self, description, done=done)
|
||||
self.func = func
|
||||
self.args, self.kwargs = args, kwargs
|
||||
self.exception = None
|
||||
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):
|
||||
self.start_time = time.time()
|
||||
self.job_manager.changed_queue.put(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.job_done(self)
|
||||
|
||||
def report_progress(self, percent, msg=''):
|
||||
self.notifications.put((percent, msg))
|
||||
@ -57,7 +57,7 @@ class DeviceJob(BaseJob):
|
||||
self.result = self.func(*self.args, **self.kwargs)
|
||||
except (Exception, SystemExit), err:
|
||||
self.failed = True
|
||||
self.details = unicode(err) + '\n\n' + \
|
||||
self._details = unicode(err) + '\n\n' + \
|
||||
traceback.format_exc()
|
||||
self.exception = err
|
||||
finally:
|
||||
@ -65,7 +65,7 @@ class DeviceJob(BaseJob):
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return cStringIO.StringIO(self.details.encode('utf-8'))
|
||||
return cStringIO.StringIO(self._details.encode('utf-8'))
|
||||
|
||||
|
||||
class DeviceManager(Thread):
|
||||
@ -230,7 +230,6 @@ class DeviceManager(Thread):
|
||||
|
||||
def _view_book(self, path, target):
|
||||
f = open(target, 'wb')
|
||||
print self.device
|
||||
self.device.get_file(path, f)
|
||||
f.close()
|
||||
return target
|
||||
@ -379,12 +378,12 @@ class DeviceMenu(QMenu):
|
||||
if action.dest == 'main:':
|
||||
action.setEnabled(True)
|
||||
elif action.dest == 'carda:0':
|
||||
if card_prefix[0] != None:
|
||||
if card_prefix and card_prefix[0] != None:
|
||||
action.setEnabled(True)
|
||||
else:
|
||||
action.setEnabled(False)
|
||||
elif action.dest == 'cardb:0':
|
||||
if card_prefix[1] != None:
|
||||
if card_prefix and card_prefix[1] != None:
|
||||
action.setEnabled(True)
|
||||
else:
|
||||
action.setEnabled(False)
|
||||
@ -737,7 +736,7 @@ class DeviceGUI(object):
|
||||
'''
|
||||
Called once metadata has been uploaded.
|
||||
'''
|
||||
if job.exception is not None:
|
||||
if job.failed:
|
||||
self.device_job_exception(job)
|
||||
return
|
||||
cp, fs = job.result
|
||||
|
@ -10,7 +10,7 @@ from PyQt4.Qt import QDialog, SIGNAL, Qt
|
||||
from calibre.gui2.dialogs.progress_ui import Ui_Dialog
|
||||
|
||||
class ProgressDialog(QDialog, Ui_Dialog):
|
||||
|
||||
|
||||
def __init__(self, title, msg='', min=0, max=99, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
@ -22,28 +22,39 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
||||
self.set_max(max)
|
||||
self.bar.setValue(min)
|
||||
self.canceled = False
|
||||
|
||||
|
||||
self.connect(self.button_box, SIGNAL('rejected()'), self._canceled)
|
||||
|
||||
|
||||
def set_msg(self, msg=''):
|
||||
self.message.setText(msg)
|
||||
|
||||
|
||||
def set_value(self, 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):
|
||||
self.bar.setMinimum(min)
|
||||
|
||||
|
||||
def set_max(self, max):
|
||||
self.bar.setMaximum(max)
|
||||
|
||||
|
||||
def _canceled(self, *args):
|
||||
self.canceled = True
|
||||
self.button_box.setDisabled(True)
|
||||
self.title.setText(_('Aborting...'))
|
||||
|
||||
self.emit(SIGNAL('canceled()'))
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == Qt.Key_Escape:
|
||||
self._canceled()
|
||||
else:
|
||||
QDialog.keyPressEvent(self, ev)
|
||||
QDialog.keyPressEvent(self, ev)
|
||||
|
@ -31,8 +31,7 @@ class JobManager(QAbstractTableModel):
|
||||
|
||||
self.jobs = []
|
||||
self.add_job = Dispatcher(self._add_job)
|
||||
self.job_done = Dispatcher(self._job_done)
|
||||
self.server = Server(self.job_done)
|
||||
self.server = Server()
|
||||
self.changed_queue = Queue()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
@ -98,7 +97,8 @@ class JobManager(QAbstractTableModel):
|
||||
try:
|
||||
self._update()
|
||||
except BaseException:
|
||||
pass
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _update(self):
|
||||
# Update running time
|
||||
@ -132,6 +132,8 @@ class JobManager(QAbstractTableModel):
|
||||
if needs_reset:
|
||||
self.jobs.sort()
|
||||
self.reset()
|
||||
if job.is_finished:
|
||||
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
|
||||
else:
|
||||
for job in jobs:
|
||||
idx = self.jobs.index(job)
|
||||
@ -155,12 +157,6 @@ class JobManager(QAbstractTableModel):
|
||||
def row_to_job(self, 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):
|
||||
for job in self.jobs:
|
||||
if job.is_running and isinstance(job, DeviceJob):
|
||||
|
@ -676,21 +676,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
'Select root folder')
|
||||
if not root:
|
||||
return
|
||||
from calibre.gui2.add import AddRecursive
|
||||
self._add_recursive_thread = AddRecursive(root,
|
||||
self.library_view.model().db, self.get_metadata,
|
||||
single, self)
|
||||
self.connect(self._add_recursive_thread, SIGNAL('finished()'),
|
||||
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
|
||||
from calibre.gui2.add import Adder
|
||||
self._adder = Adder(self,
|
||||
self.library_view.model().db,
|
||||
Dispatcher(self._files_added))
|
||||
self._adder.add_recursive(root, single)
|
||||
|
||||
def add_recursive_single(self, checked):
|
||||
'''
|
||||
@ -731,10 +721,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
(_('LRF Books'), ['lrf']),
|
||||
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
|
||||
(_('LIT Books'), ['lit']),
|
||||
(_('MOBI Books'), ['mobi', 'prc']),
|
||||
(_('MOBI Books'), ['mobi', 'prc', 'azw']),
|
||||
(_('Text books'), ['txt', 'rtf']),
|
||||
(_('PDF Books'), ['pdf']),
|
||||
(_('Comics'), ['cbz', 'cbr']),
|
||||
(_('Comics'), ['cbz', 'cbr', 'cbc']),
|
||||
(_('Archives'), ['zip', 'rar']),
|
||||
])
|
||||
if not books:
|
||||
@ -745,40 +735,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
|
||||
def _add_books(self, paths, to_device, on_card=None):
|
||||
if on_card is None:
|
||||
on_card = self.stack.currentIndex() == 2
|
||||
on_card = self.stack.currentIndex() >= 2
|
||||
if not paths:
|
||||
return
|
||||
from calibre.gui2.add import AddFiles
|
||||
self._add_files_thread = AddFiles(paths, self.default_thumbnail,
|
||||
self.get_metadata,
|
||||
None if to_device else \
|
||||
self.library_view.model().db
|
||||
)
|
||||
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()
|
||||
from calibre.gui2.add import Adder
|
||||
self._adder = Adder(self,
|
||||
None if to_device else self.library_view.model().db,
|
||||
Dispatcher(partial(self._files_added, on_card=on_card)))
|
||||
self._adder.add(paths)
|
||||
|
||||
def _files_added(self):
|
||||
t = self._add_files_thread
|
||||
self._add_files_thread = None
|
||||
if not t.canceled:
|
||||
if t.send_to_device:
|
||||
self.upload_books(t.paths,
|
||||
list(map(sanitize_file_name, t.names)),
|
||||
t.infos, on_card=t.on_card)
|
||||
self.status_bar.showMessage(
|
||||
_('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)
|
||||
def _files_added(self, paths=[], names=[], infos=[], on_card=False):
|
||||
if paths:
|
||||
self.upload_books(paths,
|
||||
list(map(sanitize_file_name, names)),
|
||||
infos, on_card=on_card)
|
||||
self.status_bar.showMessage(
|
||||
_('Uploading books to device.'), 2000)
|
||||
if self._adder.number_of_books_added > 0:
|
||||
self.library_view.model().books_added(self._adder.number_of_books_added)
|
||||
if hasattr(self, 'db_images'):
|
||||
self.db_images.reset()
|
||||
|
||||
self._adder = None
|
||||
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -1401,7 +1380,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
except:
|
||||
pass
|
||||
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()
|
||||
|
||||
def job_exception(self, job):
|
||||
@ -1525,8 +1504,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
if self.job_manager.has_device_jobs():
|
||||
msg = '<p>'+__appname__ + \
|
||||
_(''' is communicating with the device!<br>
|
||||
'Quitting may cause corruption on the device.<br>
|
||||
'Are you sure you want to quit?''')+'</p>'
|
||||
Quitting may cause corruption on the device.<br>
|
||||
Are you sure you want to quit?''')+'</p>'
|
||||
|
||||
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
|
||||
QMessageBox.Yes|QMessageBox.No, self)
|
||||
|
@ -8,6 +8,7 @@ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
|
||||
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
||||
from calibre.utils.config import OptionParser
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre import prints
|
||||
|
||||
def option_parser(usage='''\
|
||||
Usage: %prog [options]
|
||||
@ -79,8 +80,8 @@ class MainWindow(QMainWindow):
|
||||
sio = StringIO.StringIO()
|
||||
traceback.print_exception(type, value, tb, file=sio)
|
||||
fe = sio.getvalue()
|
||||
print >>sys.stderr, fe
|
||||
msg = unicode(str(value), 'utf8', 'replace')
|
||||
prints(fe, file=sys.stderr)
|
||||
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
|
||||
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
|
||||
show=True)
|
||||
except:
|
||||
|
@ -285,6 +285,7 @@ class JobsView(TableView):
|
||||
job = self.model().row_to_job(row)
|
||||
d = DetailView(self, job)
|
||||
d.exec_()
|
||||
d.timer.stop()
|
||||
|
||||
|
||||
class FontFamilyModel(QAbstractListModel):
|
||||
|
@ -1183,6 +1183,28 @@ class LibraryDatabase2(LibraryDatabase):
|
||||
path = path_or_stream
|
||||
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):
|
||||
'''
|
||||
Add a book to the database. The result cache is not updated.
|
||||
|
@ -4,9 +4,10 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import sys, os, inspect, re
|
||||
from sphinx.builder import StandaloneHTMLBuilder, bold
|
||||
from sphinx.builder import StandaloneHTMLBuilder
|
||||
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 import nodes
|
||||
|
||||
@ -181,7 +182,7 @@ def auto_member(dirname, arguments, options, content, lineno,
|
||||
docstring = '\n'.join(comment_lines)
|
||||
|
||||
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.append('.. attribute:: %s.%s'%(cls.__name__, obj), '<autodoc>')
|
||||
|
@ -17,39 +17,11 @@ E-book Format Conversion
|
||||
|
||||
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.
|
||||
|
||||
+----------------------------+------------------------------------------------------------------+
|
||||
| | **Output formats** |
|
||||
| +------------------+-----------------------+-----------------------+
|
||||
| | EPUB | LRF | MOBI |
|
||||
+===================+========+==================+=======================+=======================+
|
||||
| | MOBI | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | LIT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | PRC** | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | EPUB | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | ODT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | FB2 | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | HTML | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| **Input formats** | CBR | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | CBZ | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | RTF | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | TXT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | PDF | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | LRS | | ✔ | |
|
||||
+-------------------+--------+------------------+-----------------------+-----------------------+
|
||||
*Input Formats:* CBZ, CBR, CBC, EPUB, FB2, HTML, LIT, MOBI, ODT, PDF, PRC**, RTF, TXT
|
||||
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PDF, TXT
|
||||
|
||||
** 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.
|
||||
|
||||
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::
|
||||
|
||||
<html>
|
||||
|
@ -257,38 +257,26 @@ The final new feature is the :meth:`calibre.web.feeds.news.BasicNewsRecipe.prepr
|
||||
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
|
||||
|
||||
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
|
||||
ebook-convert myrecipe.recipe myrecipe.epub
|
||||
ebook-convert myrecipe.recipe myrecipe.mobi
|
||||
...
|
||||
|
||||
|
||||
If you just want to quickly test a couple of feeds, you can use the :option:`--feeds` option::
|
||||
|
||||
feeds2disk --feeds "['http://feeds.newsweek.com/newsweek/TopNews', 'http://feeds.newsweek.com/headlines/politics']"
|
||||
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
|
||||
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`feeds2disk`
|
||||
The command line interfce for downloading content from the internet
|
||||
|
||||
: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.
|
||||
:ref:`ebook-convert`
|
||||
The command line interface for all ebook conversion.
|
||||
|
||||
|
||||
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/>`_
|
||||
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
|
||||
--------------------
|
||||
|
@ -54,7 +54,7 @@ Customizing e-book download
|
||||
|
||||
.. automember:: BasicNewsRecipe.timefmt
|
||||
|
||||
.. automember:: basicNewsRecipe.conversion_options
|
||||
.. automember:: BasicNewsRecipe.conversion_options
|
||||
|
||||
.. automember:: BasicNewsRecipe.feeds
|
||||
|
||||
|
@ -42,7 +42,7 @@ class BaseJob(object):
|
||||
def update(self):
|
||||
if self.duration is not None:
|
||||
self._run_state = self.FINISHED
|
||||
self.percent = 1
|
||||
self.percent = 100
|
||||
if self.killed:
|
||||
self._status_text = _('Stopped')
|
||||
else:
|
||||
|
@ -25,6 +25,9 @@ PARALLEL_FUNCS = {
|
||||
|
||||
'gui_convert' :
|
||||
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
|
||||
|
||||
'read_metadata' :
|
||||
('calibre.ebooks.metadata.worker', 'read_metadata_', 'notification'),
|
||||
}
|
||||
|
||||
class Progress(Thread):
|
||||
|
Loading…
x
Reference in New Issue
Block a user