IGN:Make database a little more robust when deleting files and when detecting case sensitivity

This commit is contained in:
Kovid Goyal 2009-02-05 22:13:14 -08:00
parent 9786c65b40
commit e444b169ea
2 changed files with 215 additions and 176 deletions

View File

@ -17,21 +17,21 @@ from calibre.ebooks.chardet import xml_to_unicode
from calibre import relpath from calibre import relpath
from calibre.constants import __appname__, __version__ from calibre.constants import __appname__, __version__
from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.metadata import MetaInformation, get_parser from calibre.ebooks.metadata import MetaInformation
class Resource(object): class Resource(object):
''' '''
Represents a resource (usually a file on the filesystem or a URL pointing Represents a resource (usually a file on the filesystem or a URL pointing
to the web. Such resources are commonly referred to in OPF files. to the web. Such resources are commonly referred to in OPF files.
They have the interface: They have the interface:
:member:`path` :member:`path`
:member:`mime_type` :member:`mime_type`
:method:`href` :method:`href`
''' '''
def __init__(self, href_or_path, basedir=os.getcwd(), is_path=True): def __init__(self, href_or_path, basedir=os.getcwd(), is_path=True):
self.orig = href_or_path self.orig = href_or_path
self._href = None self._href = None
@ -63,13 +63,13 @@ class Resource(object):
pc = pc.decode('utf-8') pc = pc.decode('utf-8')
self.path = os.path.abspath(os.path.join(basedir, pc.replace('/', os.sep))) self.path = os.path.abspath(os.path.join(basedir, pc.replace('/', os.sep)))
self.fragment = url[-1] self.fragment = url[-1]
def href(self, basedir=None): def href(self, basedir=None):
''' '''
Return a URL pointing to this resource. If it is a file on the filesystem Return a URL pointing to this resource. If it is a file on the filesystem
the URL is relative to `basedir`. the URL is relative to `basedir`.
`basedir`: If None, the basedir of this resource is used (see :method:`set_basedir`). `basedir`: If None, the basedir of this resource is used (see :method:`set_basedir`).
If this resource has no basedir, then the current working directory is used as the basedir. If this resource has no basedir, then the current working directory is used as the basedir.
''' '''
@ -91,54 +91,54 @@ class Resource(object):
if isinstance(rpath, unicode): if isinstance(rpath, unicode):
rpath = rpath.encode('utf-8') rpath = rpath.encode('utf-8')
return rpath.replace(os.sep, '/')+frag return rpath.replace(os.sep, '/')+frag
def set_basedir(self, path): def set_basedir(self, path):
self._basedir = path self._basedir = path
def basedir(self): def basedir(self):
return self._basedir return self._basedir
def __repr__(self): def __repr__(self):
return 'Resource(%s, %s)'%(repr(self.path), repr(self.href())) return 'Resource(%s, %s)'%(repr(self.path), repr(self.href()))
class ResourceCollection(object): class ResourceCollection(object):
def __init__(self): def __init__(self):
self._resources = [] self._resources = []
def __iter__(self): def __iter__(self):
for r in self._resources: for r in self._resources:
yield r yield r
def __len__(self): def __len__(self):
return len(self._resources) return len(self._resources)
def __getitem__(self, index): def __getitem__(self, index):
return self._resources[index] return self._resources[index]
def __bool__(self): def __bool__(self):
return len(self._resources) > 0 return len(self._resources) > 0
def __str__(self): def __str__(self):
resources = map(repr, self) resources = map(repr, self)
return '[%s]'%', '.join(resources) return '[%s]'%', '.join(resources)
def __repr__(self): def __repr__(self):
return str(self) return str(self)
def append(self, resource): def append(self, resource):
if not isinstance(resource, Resource): if not isinstance(resource, Resource):
raise ValueError('Can only append objects of type Resource') raise ValueError('Can only append objects of type Resource')
self._resources.append(resource) self._resources.append(resource)
def remove(self, resource): def remove(self, resource):
self._resources.remove(resource) self._resources.remove(resource)
def replace(self, start, end, items): def replace(self, start, end, items):
'Same as list[start:end] = items' 'Same as list[start:end] = items'
self._resources[start:end] = items self._resources[start:end] = items
@staticmethod @staticmethod
def from_directory_contents(top, topdown=True): def from_directory_contents(top, topdown=True):
collection = ResourceCollection() collection = ResourceCollection()
@ -148,16 +148,16 @@ class ResourceCollection(object):
res.set_basedir(top) res.set_basedir(top)
collection.append(res) collection.append(res)
return collection return collection
def set_basedir(self, path): def set_basedir(self, path):
for res in self: for res in self:
res.set_basedir(path) res.set_basedir(path)
class ManifestItem(Resource): class ManifestItem(Resource):
@staticmethod @staticmethod
def from_opf_manifest_item(item, basedir): def from_opf_manifest_item(item, basedir):
href = item.get('href', None) href = item.get('href', None)
@ -167,7 +167,7 @@ class ManifestItem(Resource):
if mt: if mt:
res.mime_type = mt res.mime_type = mt
return res return res
@apply @apply
def media_type(): def media_type():
def fget(self): def fget(self):
@ -175,18 +175,18 @@ class ManifestItem(Resource):
def fset(self, val): def fset(self, val):
self.mime_type = val self.mime_type = val
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def __unicode__(self): def __unicode__(self):
return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type) return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type)
def __str__(self): def __str__(self):
return unicode(self).encode('utf-8') return unicode(self).encode('utf-8')
def __repr__(self): def __repr__(self):
return unicode(self) return unicode(self)
def __getitem__(self, index): def __getitem__(self, index):
if index == 0: if index == 0:
return self.href() return self.href()
@ -196,7 +196,7 @@ class ManifestItem(Resource):
class Manifest(ResourceCollection): class Manifest(ResourceCollection):
@staticmethod @staticmethod
def from_opf_manifest_element(items, dir): def from_opf_manifest_element(items, dir):
m = Manifest() m = Manifest()
@ -211,7 +211,7 @@ class Manifest(ResourceCollection):
except ValueError: except ValueError:
continue continue
return m return m
@staticmethod @staticmethod
def from_paths(entries): def from_paths(entries):
''' '''
@ -226,7 +226,7 @@ class Manifest(ResourceCollection):
m.next_id += 1 m.next_id += 1
m.append(mi) m.append(mi)
return m return m
def add_item(self, path, mime_type=None): def add_item(self, path, mime_type=None):
mi = ManifestItem(path, is_path=True) mi = ManifestItem(path, is_path=True)
if mime_type: if mime_type:
@ -235,37 +235,37 @@ class Manifest(ResourceCollection):
self.next_id += 1 self.next_id += 1
self.append(mi) self.append(mi)
return mi.id return mi.id
def __init__(self): def __init__(self):
ResourceCollection.__init__(self) ResourceCollection.__init__(self)
self.next_id = 1 self.next_id = 1
def item(self, id): def item(self, id):
for i in self: for i in self:
if i.id == id: if i.id == id:
return i return i
def id_for_path(self, path): def id_for_path(self, path):
path = os.path.normpath(os.path.abspath(path)) path = os.path.normpath(os.path.abspath(path))
for i in self: for i in self:
if i.path and os.path.normpath(i.path) == path: if i.path and os.path.normpath(i.path) == path:
return i.id return i.id
def path_for_id(self, id): def path_for_id(self, id):
for i in self: for i in self:
if i.id == id: if i.id == id:
return i.path return i.path
class Spine(ResourceCollection): class Spine(ResourceCollection):
class Item(Resource): class Item(Resource):
def __init__(self, idfunc, *args, **kwargs): def __init__(self, idfunc, *args, **kwargs):
Resource.__init__(self, *args, **kwargs) Resource.__init__(self, *args, **kwargs)
self.is_linear = True self.is_linear = True
self.id = idfunc(self.path) self.id = idfunc(self.path)
@staticmethod @staticmethod
def from_opf_spine_element(itemrefs, manifest): def from_opf_spine_element(itemrefs, manifest):
s = Spine(manifest) s = Spine(manifest)
@ -278,7 +278,7 @@ class Spine(ResourceCollection):
r.is_linear = itemref.get('linear', 'yes') == 'yes' r.is_linear = itemref.get('linear', 'yes') == 'yes'
s.append(r) s.append(r)
return s return s
@staticmethod @staticmethod
def from_paths(paths, manifest): def from_paths(paths, manifest):
s = Spine(manifest) s = Spine(manifest)
@ -288,14 +288,14 @@ class Spine(ResourceCollection):
except: except:
continue continue
return s return s
def __init__(self, manifest): def __init__(self, manifest):
ResourceCollection.__init__(self) ResourceCollection.__init__(self)
self.manifest = manifest self.manifest = manifest
def replace(self, start, end, ids): def replace(self, start, end, ids):
''' '''
Replace the items between start (inclusive) and end (not inclusive) with Replace the items between start (inclusive) and end (not inclusive) with
@ -308,7 +308,7 @@ class Spine(ResourceCollection):
raise ValueError('id %s not in manifest') raise ValueError('id %s not in manifest')
items.append(Spine.Item(lambda x: id, path, is_path=True)) items.append(Spine.Item(lambda x: id, path, is_path=True))
ResourceCollection.replace(start, end, items) ResourceCollection.replace(start, end, items)
def linear_items(self): def linear_items(self):
for r in self: for r in self:
if r.is_linear: if r.is_linear:
@ -318,15 +318,15 @@ class Spine(ResourceCollection):
for r in self: for r in self:
if not r.is_linear: if not r.is_linear:
yield r.path yield r.path
def items(self): def items(self):
for i in self: for i in self:
yield i.path yield i.path
class Guide(ResourceCollection): class Guide(ResourceCollection):
class Reference(Resource): class Reference(Resource):
@staticmethod @staticmethod
def from_opf_resource_item(ref, basedir): def from_opf_resource_item(ref, basedir):
title, href, type = ref.get('title', ''), ref.get('href'), ref.get('type') title, href, type = ref.get('title', ''), ref.get('href'), ref.get('type')
@ -334,14 +334,14 @@ class Guide(ResourceCollection):
res.title = title res.title = title
res.type = type res.type = type
return res return res
def __repr__(self): def __repr__(self):
ans = '<reference type="%s" href="%s" '%(self.type, self.href()) ans = '<reference type="%s" href="%s" '%(self.type, self.href())
if self.title: if self.title:
ans += 'title="%s" '%self.title ans += 'title="%s" '%self.title
return ans + '/>' return ans + '/>'
@staticmethod @staticmethod
def from_opf_guide(references, base_dir=os.getcwdu()): def from_opf_guide(references, base_dir=os.getcwdu()):
coll = Guide() coll = Guide()
@ -352,7 +352,7 @@ class Guide(ResourceCollection):
except: except:
continue continue
return coll return coll
def set_cover(self, path): def set_cover(self, path):
map(self.remove, [i for i in self if 'cover' in i.type.lower()]) map(self.remove, [i for i in self if 'cover' in i.type.lower()])
for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
@ -362,13 +362,13 @@ class Guide(ResourceCollection):
class MetadataField(object): class MetadataField(object):
def __init__(self, name, is_dc=True, formatter=None, none_is=None): def __init__(self, name, is_dc=True, formatter=None, none_is=None):
self.name = name self.name = name
self.is_dc = is_dc self.is_dc = is_dc
self.formatter = formatter self.formatter = formatter
self.none_is = none_is self.none_is = none_is
def __real_get__(self, obj, type=None): def __real_get__(self, obj, type=None):
ans = obj.get_metadata_element(self.name) ans = obj.get_metadata_element(self.name)
if ans is None: if ans is None:
@ -382,13 +382,13 @@ class MetadataField(object):
except: except:
return None return None
return ans return ans
def __get__(self, obj, type=None): def __get__(self, obj, type=None):
ans = self.__real_get__(obj, type) ans = self.__real_get__(obj, type)
if ans is None: if ans is None:
ans = self.none_is ans = self.none_is
return ans return ans
def __set__(self, obj, val): def __set__(self, obj, val):
elem = obj.get_metadata_element(self.name) elem = obj.get_metadata_element(self.name)
if elem is None: if elem is None:
@ -410,10 +410,11 @@ class OPF(object):
XPath = functools.partial(etree.XPath, namespaces=xpn) XPath = functools.partial(etree.XPath, namespaces=xpn)
CONTENT = XPath('self::*[re:match(name(), "meta$", "i")]/@content') CONTENT = XPath('self::*[re:match(name(), "meta$", "i")]/@content')
TEXT = XPath('string()') TEXT = XPath('string()')
metadata_path = XPath('descendant::*[re:match(name(), "metadata", "i")]') metadata_path = XPath('descendant::*[re:match(name(), "metadata", "i")]')
metadata_elem_path = XPath('descendant::*[re:match(name(), concat($name, "$"), "i") or (re:match(name(), "meta$", "i") and re:match(@name, concat("^calibre:", $name, "$"), "i"))]') metadata_elem_path = XPath('descendant::*[re:match(name(), concat($name, "$"), "i") or (re:match(name(), "meta$", "i") and re:match(@name, concat("^calibre:", $name, "$"), "i"))]')
title_path = XPath('descendant::*[re:match(name(), "title", "i")]')
authors_path = XPath('descendant::*[re:match(name(), "creator", "i") and (@role="aut" or @opf:role="aut" or (not(@role) and not(@opf:role)))]') authors_path = XPath('descendant::*[re:match(name(), "creator", "i") and (@role="aut" or @opf:role="aut" or (not(@role) and not(@opf:role)))]')
bkp_path = XPath('descendant::*[re:match(name(), "contributor", "i") and (@role="bkp" or @opf:role="bkp")]') bkp_path = XPath('descendant::*[re:match(name(), "contributor", "i") and (@role="bkp" or @opf:role="bkp")]')
tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]') tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]')
@ -423,10 +424,10 @@ class OPF(object):
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]') '(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]') manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]') spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
guide_path = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]') guide_path = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]')
title = MetadataField('title') title = MetadataField('title')
publisher = MetadataField('publisher') publisher = MetadataField('publisher')
language = MetadataField('language') language = MetadataField('language')
@ -435,8 +436,8 @@ class OPF(object):
series = MetadataField('series', is_dc=False) series = MetadataField('series', is_dc=False)
series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1) series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1)
rating = MetadataField('rating', is_dc=False, formatter=int) rating = MetadataField('rating', is_dc=False, formatter=int)
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True): def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True):
if not hasattr(stream, 'read'): if not hasattr(stream, 'read'):
stream = open(stream, 'rb') stream = open(stream, 'rb')
@ -463,7 +464,7 @@ class OPF(object):
self.guide = Guide.from_opf_guide(guide, basedir) if guide else None self.guide = Guide.from_opf_guide(guide, basedir) if guide else None
self.cover_data = (None, None) self.cover_data = (None, None)
self.find_toc() self.find_toc()
def find_toc(self): def find_toc(self):
self.toc = None self.toc = None
try: try:
@ -480,7 +481,7 @@ class OPF(object):
for item in self.manifest: for item in self.manifest:
if 'toc' in item.href().lower(): if 'toc' in item.href().lower():
toc = item.path toc = item.path
if toc is None: return if toc is None: return
self.toc = TOC(base_path=self.base_dir) self.toc = TOC(base_path=self.base_dir)
if toc.lower() in ('ncx', 'ncxtoc'): if toc.lower() in ('ncx', 'ncxtoc'):
@ -494,10 +495,10 @@ class OPF(object):
else: else:
self.toc.read_html_toc(toc) self.toc.read_html_toc(toc)
except: except:
pass pass
def get_text(self, elem): def get_text(self, elem):
return u''.join(self.CONTENT(elem) or self.TEXT(elem)) return u''.join(self.CONTENT(elem) or self.TEXT(elem))
@ -506,10 +507,10 @@ class OPF(object):
elem.attrib['content'] = content elem.attrib['content'] = content
else: else:
elem.text = content elem.text = content
def itermanifest(self): def itermanifest(self):
return self.manifest_path(self.root) return self.manifest_path(self.root)
def create_manifest_item(self, href, media_type): def create_manifest_item(self, href, media_type):
ids = [i.get('id', None) for i in self.itermanifest()] ids = [i.get('id', None) for i in self.itermanifest()]
id = None id = None
@ -519,11 +520,11 @@ class OPF(object):
break break
if not media_type: if not media_type:
media_type = 'application/xhtml+xml' media_type = 'application/xhtml+xml'
ans = etree.Element('{%s}item'%self.NAMESPACES['opf'], ans = etree.Element('{%s}item'%self.NAMESPACES['opf'],
attrib={'id':id, 'href':href, 'media-type':media_type}) attrib={'id':id, 'href':href, 'media-type':media_type})
ans.tail = '\n\t\t' ans.tail = '\n\t\t'
return ans return ans
def replace_manifest_item(self, item, items): def replace_manifest_item(self, item, items):
items = [self.create_manifest_item(*i) for i in items] items = [self.create_manifest_item(*i) for i in items]
for i, item2 in enumerate(items): for i, item2 in enumerate(items):
@ -532,7 +533,7 @@ class OPF(object):
index = manifest.index(item) index = manifest.index(item)
manifest[index:index+1] = items manifest[index:index+1] = items
return [i.get('id') for i in items] return [i.get('id') for i in items]
def add_path_to_manifest(self, path, media_type): def add_path_to_manifest(self, path, media_type):
has_path = False has_path = False
path = os.path.abspath(path) path = os.path.abspath(path)
@ -546,22 +547,22 @@ class OPF(object):
item = self.create_manifest_item(href, media_type) item = self.create_manifest_item(href, media_type)
manifest = self.manifest_ppath(self.root)[0] manifest = self.manifest_ppath(self.root)[0]
manifest.append(item) manifest.append(item)
def iterspine(self): def iterspine(self):
return self.spine_path(self.root) return self.spine_path(self.root)
def spine_items(self): def spine_items(self):
for item in self.iterspine(): for item in self.iterspine():
idref = item.get('idref', '') idref = item.get('idref', '')
for x in self.itermanifest(): for x in self.itermanifest():
if x.get('id', None) == idref: if x.get('id', None) == idref:
yield x.get('href', '') yield x.get('href', '')
def create_spine_item(self, idref): def create_spine_item(self, idref):
ans = etree.Element('{%s}itemref'%self.NAMESPACES['opf'], idref=idref) ans = etree.Element('{%s}itemref'%self.NAMESPACES['opf'], idref=idref)
ans.tail = '\n\t\t' ans.tail = '\n\t\t'
return ans return ans
def replace_spine_items_by_idref(self, idref, new_idrefs): def replace_spine_items_by_idref(self, idref, new_idrefs):
items = list(map(self.create_spine_item, new_idrefs)) items = list(map(self.create_spine_item, new_idrefs))
spine = self.XPath('/opf:package/*[re:match(name(), "spine", "i")]')(self.root)[0] spine = self.XPath('/opf:package/*[re:match(name(), "spine", "i")]')(self.root)[0]
@ -569,31 +570,31 @@ class OPF(object):
for x in old: for x in old:
i = spine.index(x) i = spine.index(x)
spine[i:i+1] = items spine[i:i+1] = items
def create_guide_element(self): def create_guide_element(self):
e = etree.SubElement(self.root, '{%s}guide'%self.NAMESPACES['opf']) e = etree.SubElement(self.root, '{%s}guide'%self.NAMESPACES['opf'])
e.text = '\n ' e.text = '\n '
e.tail = '\n' e.tail = '\n'
return e return e
def remove_guide(self): def remove_guide(self):
self.guide = None self.guide = None
for g in self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'}): for g in self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'}):
self.root.remove(g) self.root.remove(g)
def create_guide_item(self, type, title, href): def create_guide_item(self, type, title, href):
e = etree.Element('{%s}reference'%self.NAMESPACES['opf'], e = etree.Element('{%s}reference'%self.NAMESPACES['opf'],
type=type, title=title, href=href) type=type, title=title, href=href)
e.tail='\n' e.tail='\n'
return e return e
def add_guide_item(self, type, title, href): def add_guide_item(self, type, title, href):
g = self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'})[0] g = self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'})[0]
g.append(self.create_guide_item(type, title, href)) g.append(self.create_guide_item(type, title, href))
def iterguide(self): def iterguide(self):
return self.guide_path(self.root) return self.guide_path(self.root)
def unquote_urls(self): def unquote_urls(self):
def get_href(item): def get_href(item):
raw = unquote(item.get('href', '')) raw = unquote(item.get('href', ''))
@ -604,16 +605,16 @@ class OPF(object):
item.set('href', get_href(item)) item.set('href', get_href(item))
for item in self.iterguide(): for item in self.iterguide():
item.set('href', get_href(item)) item.set('href', get_href(item))
@apply @apply
def authors(): def authors():
def fget(self): def fget(self):
ans = [] ans = []
for elem in self.authors_path(self.metadata): for elem in self.authors_path(self.metadata):
ans.extend([x.strip() for x in self.get_text(elem).split(',')]) ans.extend([x.strip() for x in self.get_text(elem).split(',')])
return ans return ans
def fset(self, val): def fset(self, val):
remove = list(self.authors_path(self.metadata)) remove = list(self.authors_path(self.metadata))
for elem in remove: for elem in remove:
@ -622,12 +623,12 @@ class OPF(object):
attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'} attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'}
elem = self.create_metadata_element('creator', attrib=attrib) elem = self.create_metadata_element('creator', attrib=attrib)
self.set_text(elem, author) self.set_text(elem, author)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@apply @apply
def author_sort(): def author_sort():
def fget(self): def fget(self):
matches = self.authors_path(self.metadata) matches = self.authors_path(self.metadata)
if matches: if matches:
@ -637,39 +638,59 @@ class OPF(object):
ans = match.get('file-as', None) ans = match.get('file-as', None)
if ans: if ans:
return ans return ans
def fset(self, val): def fset(self, val):
matches = self.authors_path(self.metadata) matches = self.authors_path(self.metadata)
if matches: if matches:
matches[0].set('file-as', unicode(val)) matches[0].set('file-as', unicode(val))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@apply
def title_sort():
def fget(self):
matches = self.title_path(self.metadata)
if matches:
for match in matches:
ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None)
if not ans:
ans = match.get('file-as', None)
if ans:
return ans
def fset(self, val):
matches = self.title_path(self.metadata)
if matches:
matches[0].set('file-as', unicode(val))
return property(fget=fget, fset=fset)
@apply @apply
def tags(): def tags():
def fget(self): def fget(self):
ans = [] ans = []
for tag in self.tags_path(self.metadata): for tag in self.tags_path(self.metadata):
ans.append(self.get_text(tag)) ans.append(self.get_text(tag))
return ans return ans
def fset(self, val): def fset(self, val):
for tag in list(self.tags_path(self.metadata)): for tag in list(self.tags_path(self.metadata)):
self.metadata.remove(tag) self.metadata.remove(tag)
for tag in val: for tag in val:
elem = self.create_metadata_element('subject') elem = self.create_metadata_element('subject')
self.set_text(elem, unicode(tag)) self.set_text(elem, unicode(tag))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@apply @apply
def isbn(): def isbn():
def fget(self): def fget(self):
for match in self.isbn_path(self.metadata): for match in self.isbn_path(self.metadata):
return self.get_text(match) or None return self.get_text(match) or None
def fset(self, val): def fset(self, val):
matches = self.isbn_path(self.metadata) matches = self.isbn_path(self.metadata)
if not matches: if not matches:
@ -682,11 +703,11 @@ class OPF(object):
@apply @apply
def application_id(): def application_id():
def fget(self): def fget(self):
for match in self.application_id_path(self.metadata): for match in self.application_id_path(self.metadata):
return self.get_text(match) or None return self.get_text(match) or None
def fset(self, val): def fset(self, val):
matches = self.application_id_path(self.metadata) matches = self.application_id_path(self.metadata)
if not matches: if not matches:
@ -696,14 +717,14 @@ class OPF(object):
self.set_text(matches[0], unicode(val)) self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@apply @apply
def book_producer(): def book_producer():
def fget(self): def fget(self):
for match in self.bkp_path(self.metadata): for match in self.bkp_path(self.metadata):
return self.get_text(match) or None return self.get_text(match) or None
def fset(self, val): def fset(self, val):
matches = self.bkp_path(self.metadata) matches = self.bkp_path(self.metadata)
if not matches: if not matches:
@ -712,8 +733,8 @@ class OPF(object):
attrib=attrib)] attrib=attrib)]
self.set_text(matches[0], unicode(val)) self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def guess_cover(self): def guess_cover(self):
''' '''
Try to guess a cover. Needed for some old/badly formed OPF files. Try to guess a cover. Needed for some old/badly formed OPF files.
@ -733,11 +754,11 @@ class OPF(object):
cpath = os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK) cpath = os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK)
if os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK): if os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK):
return cpath return cpath
@apply @apply
def cover(): def cover():
def fget(self): def fget(self):
if self.guide is not None: if self.guide is not None:
for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
@ -748,19 +769,19 @@ class OPF(object):
return self.guess_cover() return self.guess_cover()
except: except:
pass pass
def fset(self, path): def fset(self, path):
if self.guide is not None: if self.guide is not None:
self.guide.set_cover(path) self.guide.set_cover(path)
for item in list(self.iterguide()): for item in list(self.iterguide()):
if 'cover' in item.get('type', ''): if 'cover' in item.get('type', ''):
item.getparent().remove(item) item.getparent().remove(item)
else: else:
g = self.create_guide_element() g = self.create_guide_element()
self.guide = Guide() self.guide = Guide()
self.guide.set_cover(path) self.guide.set_cover(path)
etree.SubElement(g, 'opf:reference', nsmap=self.NAMESPACES, etree.SubElement(g, 'opf:reference', nsmap=self.NAMESPACES,
attrib={'type':'cover', 'href':self.guide[-1].href()}) attrib={'type':'cover', 'href':self.guide[-1].href()})
id = self.manifest.id_for_path(self.cover) id = self.manifest.id_for_path(self.cover)
if id is None: if id is None:
@ -768,14 +789,14 @@ class OPF(object):
for item in self.guide: for item in self.guide:
if item.type.lower() == t: if item.type.lower() == t:
self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0]) self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0])
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def get_metadata_element(self, name): def get_metadata_element(self, name):
matches = self.metadata_elem_path(self.metadata, name=name) matches = self.metadata_elem_path(self.metadata, name=name)
if matches: if matches:
return matches[-1] return matches[-1]
def create_metadata_element(self, name, attrib=None, is_dc=True): def create_metadata_element(self, name, attrib=None, is_dc=True):
if is_dc: if is_dc:
name = '{%s}%s' % (self.NAMESPACES['dc'], name) name = '{%s}%s' % (self.NAMESPACES['dc'], name)
@ -787,24 +808,25 @@ class OPF(object):
nsmap=self.NAMESPACES) nsmap=self.NAMESPACES)
elem.tail = '\n' elem.tail = '\n'
return elem return elem
def render(self, encoding='utf-8'): def render(self, encoding='utf-8'):
raw = etree.tostring(self.root, encoding=encoding, pretty_print=True) raw = etree.tostring(self.root, encoding=encoding, pretty_print=True)
if not raw.lstrip().startswith('<?xml '): if not raw.lstrip().startswith('<?xml '):
raw = '<?xml version="1.0" encoding="%s"?>\n'%encoding.upper()+raw raw = '<?xml version="1.0" encoding="%s"?>\n'%encoding.upper()+raw
return raw return raw
def smart_update(self, mi): def smart_update(self, mi):
for attr in ('author_sort', 'title_sort', 'comments', 'category', for attr in ('title', 'authors', 'author_sort', 'title_sort',
'publisher', 'series', 'series_index', 'rating', 'publisher', 'series', 'series_index', 'rating',
'isbn', 'language', 'tags', 'title', 'authors'): 'isbn', 'language', 'tags', 'category', 'comments'):
val = getattr(mi, attr, None) val = getattr(mi, attr, None)
if val is not None and val != [] and val != (None, None): if val is not None and val != [] and val != (None, None):
setattr(self, attr, val) setattr(self, attr, val)
print self.render()
class OPFCreator(MetaInformation): class OPFCreator(MetaInformation):
def __init__(self, base_path, *args, **kwargs): def __init__(self, base_path, *args, **kwargs):
''' '''
Initialize. Initialize.
@ -824,63 +846,63 @@ class OPFCreator(MetaInformation):
self.guide = Guide() self.guide = Guide()
if self.cover: if self.cover:
self.guide.set_cover(self.cover) self.guide.set_cover(self.cover)
def create_manifest(self, entries): def create_manifest(self, entries):
''' '''
Create <manifest> Create <manifest>
`entries`: List of (path, mime-type) If mime-type is None it is autodetected `entries`: List of (path, mime-type) If mime-type is None it is autodetected
''' '''
entries = map(lambda x: x if os.path.isabs(x[0]) else entries = map(lambda x: x if os.path.isabs(x[0]) else
(os.path.abspath(os.path.join(self.base_path, x[0])), x[1]), (os.path.abspath(os.path.join(self.base_path, x[0])), x[1]),
entries) entries)
self.manifest = Manifest.from_paths(entries) self.manifest = Manifest.from_paths(entries)
self.manifest.set_basedir(self.base_path) self.manifest.set_basedir(self.base_path)
def create_manifest_from_files_in(self, files_and_dirs): def create_manifest_from_files_in(self, files_and_dirs):
entries = [] entries = []
def dodir(dir): def dodir(dir):
for spec in os.walk(dir): for spec in os.walk(dir):
root, files = spec[0], spec[-1] root, files = spec[0], spec[-1]
for name in files: for name in files:
path = os.path.join(root, name) path = os.path.join(root, name)
if os.path.isfile(path): if os.path.isfile(path):
entries.append((path, None)) entries.append((path, None))
for i in files_and_dirs: for i in files_and_dirs:
if os.path.isdir(i): if os.path.isdir(i):
dodir(i) dodir(i)
else: else:
entries.append((i, None)) entries.append((i, None))
self.create_manifest(entries) self.create_manifest(entries)
def create_spine(self, entries): def create_spine(self, entries):
''' '''
Create the <spine> element. Must first call :method:`create_manifest`. Create the <spine> element. Must first call :method:`create_manifest`.
`entries`: List of paths `entries`: List of paths
''' '''
entries = map(lambda x: x if os.path.isabs(x) else entries = map(lambda x: x if os.path.isabs(x) else
os.path.abspath(os.path.join(self.base_path, x)), entries) os.path.abspath(os.path.join(self.base_path, x)), entries)
self.spine = Spine.from_paths(entries, self.manifest) self.spine = Spine.from_paths(entries, self.manifest)
def set_toc(self, toc): def set_toc(self, toc):
''' '''
Set the toc. You must call :method:`create_spine` before calling this Set the toc. You must call :method:`create_spine` before calling this
method. method.
:param toc: A :class:`TOC` object :param toc: A :class:`TOC` object
''' '''
self.toc = toc self.toc = toc
def create_guide(self, guide_element): def create_guide(self, guide_element):
self.guide = Guide.from_opf_guide(guide_element, self.base_path) self.guide = Guide.from_opf_guide(guide_element, self.base_path)
self.guide.set_basedir(self.base_path) self.guide.set_basedir(self.base_path)
def render(self, opf_stream=sys.stdout, ncx_stream=None, def render(self, opf_stream=sys.stdout, ncx_stream=None,
ncx_manifest_entry=None): ncx_manifest_entry=None):
from calibre.resources import opf_template from calibre.resources import opf_template
from calibre.utils.genshi.template import MarkupTemplate from calibre.utils.genshi.template import MarkupTemplate
@ -914,14 +936,14 @@ class OPFCreator(MetaInformation):
class OPFTest(unittest.TestCase): class OPFTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.stream = cStringIO.StringIO( self.stream = cStringIO.StringIO(
'''\ '''\
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<package version="2.0" xmlns="http://www.idpf.org/2007/opf" > <package version="2.0" xmlns="http://www.idpf.org/2007/opf" >
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title>A Cool &amp; &copy; &#223; Title</dc:title> <dc:title opf:file-as="Wow">A Cool &amp; &copy; &#223; Title</dc:title>
<creator opf:role="aut" file-as="Monkey">Monkey Kitchen, Next</creator> <creator opf:role="aut" file-as="Monkey">Monkey Kitchen, Next</creator>
<dc:subject>One</dc:subject><dc:subject>Two</dc:subject> <dc:subject>One</dc:subject><dc:subject>Two</dc:subject>
<dc:identifier scheme="ISBN">123456789</dc:identifier> <dc:identifier scheme="ISBN">123456789</dc:identifier>
@ -936,33 +958,48 @@ class OPFTest(unittest.TestCase):
''' '''
) )
self.opf = OPF(self.stream, os.getcwd()) self.opf = OPF(self.stream, os.getcwd())
def testReading(self): def testReading(self, opf=None):
opf = self.opf if opf is None:
opf = self.opf
self.assertEqual(opf.title, u'A Cool & \xa9 \xdf Title') self.assertEqual(opf.title, u'A Cool & \xa9 \xdf Title')
self.assertEqual(opf.authors, u'Monkey Kitchen,Next'.split(',')) self.assertEqual(opf.authors, u'Monkey Kitchen,Next'.split(','))
self.assertEqual(opf.author_sort, 'Monkey') self.assertEqual(opf.author_sort, 'Monkey')
self.assertEqual(opf.title_sort, 'Wow')
self.assertEqual(opf.tags, ['One', 'Two']) self.assertEqual(opf.tags, ['One', 'Two'])
self.assertEqual(opf.isbn, '123456789') self.assertEqual(opf.isbn, '123456789')
self.assertEqual(opf.series, 'A one book series') self.assertEqual(opf.series, 'A one book series')
self.assertEqual(opf.series_index, None) self.assertEqual(opf.series_index, None)
self.assertEqual(list(opf.itermanifest())[0].get('href'), 'a ~ b') self.assertEqual(list(opf.itermanifest())[0].get('href'), 'a ~ b')
def testWriting(self): def testWriting(self):
for test in [('title', 'New & Title'), ('authors', ['One', 'Two']), for test in [('title', 'New & Title'), ('authors', ['One', 'Two']),
('author_sort', "Kitchen"), ('tags', ['Three']), ('author_sort', "Kitchen"), ('tags', ['Three']),
('isbn', 'a'), ('rating', 3), ('series_index', 1)]: ('isbn', 'a'), ('rating', 3), ('series_index', 1),
('title_sort', 'ts')]:
setattr(self.opf, *test) setattr(self.opf, *test)
self.assertEqual(getattr(self.opf, test[0]), test[1]) self.assertEqual(getattr(self.opf, test[0]), test[1])
self.opf.render() self.opf.render()
def testCreator(self):
opf = OPFCreator(os.getcwd(), self.opf)
buf = cStringIO.StringIO()
opf.render(buf)
raw = buf.getvalue()
self.testReading(opf=OPF(cStringIO.StringIO(raw), os.getcwd()))
def testSmartUpdate(self):
self.opf.smart_update(self.opf)
self.testReading()
def suite(): def suite():
return unittest.TestLoader().loadTestsFromTestCase(OPFTest) return unittest.TestLoader().loadTestsFromTestCase(OPFTest)
def test(): def test():
unittest.TextTestRunner(verbosity=2).run(suite()) unittest.TextTestRunner(verbosity=2).run(suite())
def option_parser(): def option_parser():
parser = get_parser('opf') parser = get_parser('opf')
parser.add_option('--language', default=None, help=_('Set the dc:language field')) parser.add_option('--language', default=None, help=_('Set the dc:language field'))
@ -1010,7 +1047,7 @@ def main(args=sys.argv):
print MetaInformation(OPF(open(opfpath, 'rb'), basedir)) print MetaInformation(OPF(open(opfpath, 'rb'), basedir))
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.exit(main())

View File

@ -15,7 +15,7 @@ from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock
from PyQt4.QtGui import QApplication, QPixmap, QImage from PyQt4.QtGui import QApplication, QPixmap, QImage
__app = None __app = None
from calibre.library import title_sort from calibre.ebooks.metadata import title_sort
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.library.sqlite import connect, IntegrityError from calibre.library.sqlite import connect, IntegrityError
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
@ -27,15 +27,6 @@ from calibre.customize.ui import run_plugins_on_import
from calibre import sanitize_file_name from calibre import sanitize_file_name
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
iscaseinsensitive = iswindows or isosx
def normpath(x):
# The builtin os.path.normcase doesn't work on OS X
x = os.path.abspath(x)
if iscaseinsensitive:
x = x.lower()
return x
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5, FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10, 'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
@ -355,7 +346,8 @@ class LibraryDatabase2(LibraryDatabase):
if isinstance(self.dbpath, unicode): if isinstance(self.dbpath, unicode):
self.dbpath = self.dbpath.encode(filesystem_encoding) self.dbpath = self.dbpath.encode(filesystem_encoding)
self.connect() self.connect()
self.is_case_sensitive = not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) self.is_case_sensitive = not iswindows and not isosx and \
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB'))
# Upgrade database # Upgrade database
while True: while True:
meth = getattr(self, 'upgrade_version_%d'%self.user_version, None) meth = getattr(self, 'upgrade_version_%d'%self.user_version, None)
@ -489,6 +481,16 @@ class LibraryDatabase2(LibraryDatabase):
name = title + ' - ' + author name = title + ' - ' + author
return name return name
def rmtree(self, path):
if not self.normpath(self.library_path).startswith(self.normpath(path)):
shutil.rmtree(path)
def normpath(self, path):
path = os.path.abspath(os.path.realpath(path))
if not self.is_case_sensitive:
path = path.lower()
return path
def set_path(self, index, index_is_id=False): def set_path(self, index, index_is_id=False):
''' '''
Set the path to the directory containing this books files based on its Set the path to the directory containing this books files based on its
@ -532,11 +534,11 @@ class LibraryDatabase2(LibraryDatabase):
self.data.set(id, FIELD_MAP['path'], path, row_is_id=True) self.data.set(id, FIELD_MAP['path'], path, row_is_id=True)
# Delete not needed directories # Delete not needed directories
if current_path and os.path.exists(spath): if current_path and os.path.exists(spath):
if normpath(spath) != normpath(tpath): if self.normpath(spath) != self.normpath(tpath):
shutil.rmtree(spath) self.rmtree(spath)
parent = os.path.dirname(spath) parent = os.path.dirname(spath)
if len(os.listdir(parent)) == 0: if len(os.listdir(parent)) == 0:
shutil.rmtree(parent) self.rmtree(parent)
def add_listener(self, listener): def add_listener(self, listener):
''' '''
@ -699,10 +701,10 @@ class LibraryDatabase2(LibraryDatabase):
path = os.path.join(self.library_path, self.path(id, index_is_id=True)) path = os.path.join(self.library_path, self.path(id, index_is_id=True))
self.data.remove(id) self.data.remove(id)
if os.path.exists(path): if os.path.exists(path):
shutil.rmtree(path) self.rmtree(path)
parent = os.path.dirname(path) parent = os.path.dirname(path)
if len(os.listdir(parent)) == 0: if len(os.listdir(parent)) == 0:
shutil.rmtree(parent) self.rmtree(parent)
self.conn.execute('DELETE FROM books WHERE id=?', (id,)) self.conn.execute('DELETE FROM books WHERE id=?', (id,))
self.conn.commit() self.conn.commit()
self.clean() self.clean()