Make series index a float and add a column for publish date

This commit is contained in:
Kovid Goyal 2009-05-30 23:37:05 -07:00
parent 597fd045a9
commit 3585977bb3
18 changed files with 465 additions and 369 deletions

View File

@ -42,6 +42,31 @@ def title_sort(title):
title = title.replace(prep, '') + ', ' + prep title = title.replace(prep, '') + ', ' + prep
return title.strip() return title.strip()
coding = zip(
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
)
def roman(num):
if num <= 0 or num >= 4000 or int(num) != num:
return str(num)
result = []
for d, r in coding:
while num >= d:
result.append(r)
num -= d
return ''.join(result)
def fmt_sidx(i, fmt='%.2f', use_roman=False):
if i is None:
i = 1
if int(i) == i:
return roman(i) if use_roman else '%d'%i
return fmt%i
class Resource(object): class Resource(object):
''' '''
@ -187,7 +212,8 @@ class MetaInformation(object):
'publisher', 'series', 'series_index', 'rating', 'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id', 'guide', 'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language', 'manifest', 'spine', 'toc', 'cover', 'language',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc'): 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
'pubdate'):
if hasattr(mi, attr): if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr)) setattr(ans, attr, getattr(mi, attr))
@ -212,7 +238,7 @@ class MetaInformation(object):
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher', for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language', 'series', 'series_index', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc' 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate'
): ):
setattr(self, x, getattr(mi, x, None)) setattr(self, x, getattr(mi, x, None))
@ -231,7 +257,7 @@ class MetaInformation(object):
'publisher', 'series', 'series_index', 'rating', 'publisher', 'series', 'series_index', 'rating',
'isbn', 'application_id', 'manifest', 'spine', 'toc', 'isbn', 'application_id', 'manifest', 'spine', 'toc',
'cover', 'language', 'guide', 'book_producer', 'cover', 'language', 'guide', 'book_producer',
'timestamp', 'lccn', 'lcc', 'ddc'): 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate'):
if hasattr(mi, attr): if hasattr(mi, attr):
val = getattr(mi, attr) val = getattr(mi, attr)
if val is not None: if val is not None:
@ -262,8 +288,8 @@ class MetaInformation(object):
try: try:
x = float(self.series_index) x = float(self.series_index)
except ValueError: except ValueError:
x = 1.0 x = 1
return '%d'%x if int(x) == x else '%.2f'%x return fmt_sidx(x)
def authors_from_string(self, raw): def authors_from_string(self, raw):
self.authors = string_to_authors(raw) self.authors = string_to_authors(raw)
@ -299,6 +325,8 @@ class MetaInformation(object):
fmt('Rating', self.rating) fmt('Rating', self.rating)
if self.timestamp is not None: if self.timestamp is not None:
fmt('Timestamp', self.timestamp.isoformat(' ')) fmt('Timestamp', self.timestamp.isoformat(' '))
if self.pubdate is not None:
fmt('Published', self.pubdate.isoformat(' '))
if self.lccn: if self.lccn:
fmt('LCCN', unicode(self.lccn)) fmt('LCCN', unicode(self.lccn))
if self.lcc: if self.lcc:
@ -327,6 +355,8 @@ class MetaInformation(object):
ans += [(_('Language'), unicode(self.language))] ans += [(_('Language'), unicode(self.language))]
if self.timestamp is not None: if self.timestamp is not None:
ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))] ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
if self.pubdate is not None:
ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
for i, x in enumerate(ans): for i, x in enumerate(ans):
ans[i] = u'<tr><td><b>%s</b></td><td>%s</td></tr>'%x ans[i] = u'<tr><td><b>%s</b></td><td>%s</td></tr>'%x
return u'<table>%s</table>'%u'\n'.join(ans) return u'<table>%s</table>'%u'\n'.join(ans)

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Anatoly Shipitsin <norguhtar at gmail.com>'
'''Read meta information from fb2 files''' '''Read meta information from fb2 files'''
import sys, os, mimetypes import mimetypes
from base64 import b64decode from base64 import b64decode
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
@ -32,7 +32,7 @@ def get_metadata(stream):
if not exts: if not exts:
exts = ['.jpg'] exts = ['.jpg']
cdata = (exts[0][1:], b64decode(binary.string.strip())) cdata = (exts[0][1:], b64decode(binary.string.strip()))
if comments: if comments:
comments = u''.join(comments.findAll(text=True)) comments = u''.join(comments.findAll(text=True))
series = soup.find("sequence") series = soup.find("sequence")
@ -42,7 +42,7 @@ def get_metadata(stream):
if series: if series:
mi.series = series.get('name', None) mi.series = series.get('name', None)
try: try:
mi.series_index = int(series.get('number', None)) mi.series_index = float(series.get('number', None))
except (TypeError, ValueError): except (TypeError, ValueError):
pass pass
if cdata: if cdata:

View File

@ -145,7 +145,7 @@ def metadata_from_filename(name, pat=None):
pass pass
try: try:
si = match.group('series_index') si = match.group('series_index')
mi.series_index = int(si) mi.series_index = float(si)
except (IndexError, ValueError, TypeError): except (IndexError, ValueError, TypeError):
pass pass
try: try:

View File

@ -2,8 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read/Write metadata from Open Packaging Format (.opf) files.''' '''Read/Write metadata from Open Packaging Format (.opf) files.'''
import sys, re, os, glob import re, os
import cStringIO
import uuid import uuid
from urllib import unquote, quote from urllib import unquote, quote
@ -15,14 +14,14 @@ from calibre.ebooks.metadata import Resource, ResourceCollection
from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.metadata.toc import TOC
class OPFSoup(BeautifulStoneSoup): class OPFSoup(BeautifulStoneSoup):
def __init__(self, raw): def __init__(self, raw):
BeautifulStoneSoup.__init__(self, raw, BeautifulStoneSoup.__init__(self, raw,
convertEntities=BeautifulSoup.HTML_ENTITIES, convertEntities=BeautifulSoup.HTML_ENTITIES,
selfClosingTags=['item', 'itemref', 'reference']) selfClosingTags=['item', 'itemref', 'reference'])
class ManifestItem(Resource): class ManifestItem(Resource):
@staticmethod @staticmethod
def from_opf_manifest_item(item, basedir): def from_opf_manifest_item(item, basedir):
if item.has_key('href'): if item.has_key('href'):
@ -37,7 +36,7 @@ class ManifestItem(Resource):
if mt: if mt:
res.mime_type = mt res.mime_type = mt
return res return res
@dynamic_property @dynamic_property
def media_type(self): def media_type(self):
def fget(self): def fget(self):
@ -45,28 +44,28 @@ 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()
if index == 1: if index == 1:
return self.media_type return self.media_type
raise IndexError('%d out of bounds.'%index) raise IndexError('%d out of bounds.'%index)
class Manifest(ResourceCollection): class Manifest(ResourceCollection):
@staticmethod @staticmethod
def from_opf_manifest_element(manifest, dir): def from_opf_manifest_element(manifest, dir):
m = Manifest() m = Manifest()
@ -81,7 +80,7 @@ class Manifest(ResourceCollection):
except ValueError: except ValueError:
continue continue
return m return m
@staticmethod @staticmethod
def from_paths(entries): def from_paths(entries):
''' '''
@ -96,37 +95,37 @@ class Manifest(ResourceCollection):
m.next_id += 1 m.next_id += 1
m.append(mi) m.append(mi)
return m return m
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(spine, manifest): def from_opf_spine_element(spine, manifest):
s = Spine(manifest) s = Spine(manifest)
@ -137,7 +136,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)
@ -147,14 +146,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 linear_items(self): def linear_items(self):
for r in self: for r in self:
if r.is_linear: if r.is_linear:
@ -164,16 +163,16 @@ 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['href'], ref['type'] title, href, type = ref.get('title', ''), ref['href'], ref['type']
@ -181,14 +180,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(guide_elem, base_dir=os.getcwdu()): def from_opf_guide(guide_elem, base_dir=os.getcwdu()):
coll = Guide() coll = Guide()
@ -199,29 +198,29 @@ 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'):
self.append(Guide.Reference(path, is_path=True)) self.append(Guide.Reference(path, is_path=True))
self[-1].type = type self[-1].type = type
self[-1].title = '' self[-1].title = ''
class standard_field(object): class standard_field(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def __get__(self, obj, typ=None): def __get__(self, obj, typ=None):
return getattr(obj, 'get_'+self.name)() return getattr(obj, 'get_'+self.name)()
class OPF(MetaInformation): class OPF(MetaInformation):
MIMETYPE = 'application/oebps-package+xml' MIMETYPE = 'application/oebps-package+xml'
ENTITY_PATTERN = re.compile(r'&(\S+?);') ENTITY_PATTERN = re.compile(r'&(\S+?);')
uid = standard_field('uid') uid = standard_field('uid')
application_id = standard_field('application_id') application_id = standard_field('application_id')
title = standard_field('title') title = standard_field('title')
@ -238,29 +237,29 @@ class OPF(MetaInformation):
series_index = standard_field('series_index') series_index = standard_field('series_index')
rating = standard_field('rating') rating = standard_field('rating')
tags = standard_field('tags') tags = standard_field('tags')
def __init__(self): def __init__(self):
raise NotImplementedError('Abstract base class') raise NotImplementedError('Abstract base class')
@dynamic_property @dynamic_property
def package(self): def package(self):
def fget(self): def fget(self):
return self.soup.find(re.compile('package')) return self.soup.find(re.compile('package'))
return property(fget=fget) return property(fget=fget)
@dynamic_property @dynamic_property
def metadata(self): def metadata(self):
def fget(self): def fget(self):
return self.package.find(re.compile('metadata')) return self.package.find(re.compile('metadata'))
return property(fget=fget) return property(fget=fget)
def get_title(self): def get_title(self):
title = self.metadata.find('dc:title') title = self.metadata.find('dc:title')
if title and title.string: if title and title.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip() return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
return self.default_title.strip() return self.default_title.strip()
def get_authors(self): def get_authors(self):
creators = self.metadata.findAll('dc:creator') creators = self.metadata.findAll('dc:creator')
for elem in creators: for elem in creators:
@ -277,7 +276,7 @@ class OPF(MetaInformation):
ans.extend(i.split('&')) ans.extend(i.split('&'))
return [a.strip() for a in ans] return [a.strip() for a in ans]
return [] return []
def get_author_sort(self): def get_author_sort(self):
creators = self.metadata.findAll('dc:creator') creators = self.metadata.findAll('dc:creator')
for elem in creators: for elem in creators:
@ -288,37 +287,37 @@ class OPF(MetaInformation):
fa = elem.get('file-as') fa = elem.get('file-as')
return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None
return None return None
def get_title_sort(self): def get_title_sort(self):
title = self.package.find('dc:title') title = self.package.find('dc:title')
if title: if title:
if title.has_key('file-as'): if title.has_key('file-as'):
return title['file-as'].strip() return title['file-as'].strip()
return None return None
def get_comments(self): def get_comments(self):
comments = self.soup.find('dc:description') comments = self.soup.find('dc:description')
if comments and comments.string: if comments and comments.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip() return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip()
return None return None
def get_uid(self): def get_uid(self):
package = self.package package = self.package
if package.has_key('unique-identifier'): if package.has_key('unique-identifier'):
return package['unique-identifier'] return package['unique-identifier']
def get_category(self): def get_category(self):
category = self.soup.find('dc:type') category = self.soup.find('dc:type')
if category and category.string: if category and category.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip() return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
return None return None
def get_publisher(self): def get_publisher(self):
publisher = self.soup.find('dc:publisher') publisher = self.soup.find('dc:publisher')
if publisher and publisher.string: if publisher and publisher.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip() return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
return None return None
def get_isbn(self): def get_isbn(self):
for item in self.metadata.findAll('dc:identifier'): for item in self.metadata.findAll('dc:identifier'):
scheme = item.get('scheme') scheme = item.get('scheme')
@ -327,13 +326,13 @@ class OPF(MetaInformation):
if scheme is not None and scheme.lower() == 'isbn' and item.string: if scheme is not None and scheme.lower() == 'isbn' and item.string:
return str(item.string).strip() return str(item.string).strip()
return None return None
def get_language(self): def get_language(self):
item = self.metadata.find('dc:language') item = self.metadata.find('dc:language')
if not item: if not item:
return _('Unknown') return _('Unknown')
return ''.join(item.findAll(text=True)).strip() return ''.join(item.findAll(text=True)).strip()
def get_application_id(self): def get_application_id(self):
for item in self.metadata.findAll('dc:identifier'): for item in self.metadata.findAll('dc:identifier'):
scheme = item.get('scheme', None) scheme = item.get('scheme', None)
@ -342,7 +341,7 @@ class OPF(MetaInformation):
if scheme in ['libprs500', 'calibre']: if scheme in ['libprs500', 'calibre']:
return str(item.string).strip() return str(item.string).strip()
return None return None
def get_cover(self): def get_cover(self):
guide = getattr(self, 'guide', []) guide = getattr(self, 'guide', [])
if not guide: if not guide:
@ -352,7 +351,7 @@ class OPF(MetaInformation):
matches = [r for r in references if r.type.lower() == candidate and r.path] matches = [r for r in references if r.type.lower() == candidate and r.path]
if matches: if matches:
return matches[0].path return matches[0].path
def possible_cover_prefixes(self): def possible_cover_prefixes(self):
isbn, ans = [], [] isbn, ans = [], []
for item in self.metadata.findAll('dc:identifier'): for item in self.metadata.findAll('dc:identifier'):
@ -363,22 +362,22 @@ class OPF(MetaInformation):
for item in isbn: for item in isbn:
ans.append(item[1].replace('-', '')) ans.append(item[1].replace('-', ''))
return ans return ans
def get_series(self): def get_series(self):
s = self.metadata.find('series') s = self.metadata.find('series')
if s is not None: if s is not None:
return str(s.string).strip() return str(s.string).strip()
return None return None
def get_series_index(self): def get_series_index(self):
s = self.metadata.find('series-index') s = self.metadata.find('series-index')
if s and s.string: if s and s.string:
try: try:
return int(str(s.string).strip()) return float(str(s.string).strip())
except: except:
return None return None
return None return None
def get_rating(self): def get_rating(self):
s = self.metadata.find('rating') s = self.metadata.find('rating')
if s and s.string: if s and s.string:
@ -387,7 +386,7 @@ class OPF(MetaInformation):
except: except:
return None return None
return None return None
def get_tags(self): def get_tags(self):
ans = [] ans = []
subs = self.soup.findAll('dc:subject') subs = self.soup.findAll('dc:subject')
@ -396,17 +395,17 @@ class OPF(MetaInformation):
if val: if val:
ans.append(val) ans.append(val)
return [unicode(a).strip() for a in ans] return [unicode(a).strip() for a in ans]
class OPFReader(OPF): class OPFReader(OPF):
def __init__(self, stream, dir=os.getcwdu()): def __init__(self, stream, dir=os.getcwdu()):
manage = False manage = False
if not hasattr(stream, 'read'): if not hasattr(stream, 'read'):
manage = True manage = True
dir = os.path.dirname(stream) dir = os.path.dirname(stream)
stream = open(stream, 'rb') stream = open(stream, 'rb')
self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown' self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown'
if hasattr(stream, 'seek'): if hasattr(stream, 'seek'):
stream.seek(0) stream.seek(0)
self.soup = OPFSoup(stream.read()) self.soup = OPFSoup(stream.read())
@ -420,18 +419,18 @@ class OPFReader(OPF):
spine = self.soup.find(re.compile('spine')) spine = self.soup.find(re.compile('spine'))
if spine is not None: if spine is not None:
self.spine = Spine.from_opf_spine_element(spine, self.manifest) self.spine = Spine.from_opf_spine_element(spine, self.manifest)
self.toc = TOC(base_path=dir) self.toc = TOC(base_path=dir)
self.toc.read_from_opf(self) self.toc.read_from_opf(self)
guide = self.soup.find(re.compile('guide')) guide = self.soup.find(re.compile('guide'))
if guide is not None: if guide is not None:
self.guide = Guide.from_opf_guide(guide, dir) self.guide = Guide.from_opf_guide(guide, dir)
self.base_dir = dir self.base_dir = dir
self.cover_data = (None, None) self.cover_data = (None, None)
class OPFCreator(MetaInformation): class OPFCreator(MetaInformation):
def __init__(self, base_path, *args, **kwargs): def __init__(self, base_path, *args, **kwargs):
''' '''
Initialize. Initialize.
@ -451,62 +450,62 @@ 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, ncx_stream=None, ncx_manifest_entry=None): def render(self, opf_stream, ncx_stream=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
@ -530,7 +529,7 @@ class OPFCreator(MetaInformation):
cover = os.path.abspath(os.path.join(self.base_path, cover)) cover = os.path.abspath(os.path.join(self.base_path, cover))
self.guide.set_cover(cover) self.guide.set_cover(cover)
self.guide.set_basedir(self.base_path) self.guide.set_basedir(self.base_path)
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml') opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
if not opf.startswith('<?xml '): if not opf.startswith('<?xml '):
opf = '<?xml version="1.0" encoding="UTF-8"?>\n'+opf opf = '<?xml version="1.0" encoding="UTF-8"?>\n'+opf
@ -540,4 +539,4 @@ class OPFCreator(MetaInformation):
if toc is not None and ncx_stream is not None: if toc is not None and ncx_stream is not None:
toc.render(ncx_stream, self.application_id) toc.render(ncx_stream, self.application_id)
ncx_stream.flush() ncx_stream.flush()

View File

@ -9,15 +9,16 @@
<dc:creator opf:role="aut" py:for="i, author in enumerate(mi.authors)" py:attrs="{'opf:file-as':mi.author_sort} if mi.author_sort and i == 0 else {}">${author}</dc:creator> <dc:creator opf:role="aut" py:for="i, author in enumerate(mi.authors)" py:attrs="{'opf:file-as':mi.author_sort} if mi.author_sort and i == 0 else {}">${author}</dc:creator>
<dc:contributor opf:role="bkp" py:with="attrs={'opf:file-as':__appname__}" py:attrs="attrs">${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]</dc:contributor> <dc:contributor opf:role="bkp" py:with="attrs={'opf:file-as':__appname__}" py:attrs="attrs">${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]</dc:contributor>
<dc:identifier opf:scheme="${__appname__}" id="${__appname__}_id">${mi.application_id}</dc:identifier> <dc:identifier opf:scheme="${__appname__}" id="${__appname__}_id">${mi.application_id}</dc:identifier>
<dc:date py:if="getattr(mi, 'timestamp', None) is not None">${mi.timestamp.isoformat()}</dc:date> <dc:date py:if="getattr(mi, 'pubdate', None) is not None">${mi.pubdate.isoformat()}</dc:date>
<dc:language>${mi.language if mi.language else 'UND'}</dc:language> <dc:language>${mi.language if mi.language else 'UND'}</dc:language>
<dc:type py:if="getattr(mi, 'category', False)">${mi.category}</dc:type> <dc:type py:if="getattr(mi, 'category', False)">${mi.category}</dc:type>
<dc:description py:if="mi.comments">${mi.comments}</dc:description> <dc:description py:if="mi.comments">${mi.comments}</dc:description>
<dc:publisher py:if="mi.publisher">${mi.publisher}</dc:publisher> <dc:publisher py:if="mi.publisher">${mi.publisher}</dc:publisher>
<dc:identifier opf:scheme="ISBN" py:if="mi.isbn">${mi.isbn}</dc:identifier> <dc:identifier opf:scheme="ISBN" py:if="mi.isbn">${mi.isbn}</dc:identifier>
<meta py:if="mi.series is not None" name="calibre:series" content="${mi.series}"/> <meta py:if="mi.series is not None" name="calibre:series" content="${mi.series}"/>
<meta py:if="mi.series_index is not None" name="calibre:series_index" content="${mi.series_index}"/> <meta py:if="mi.series_index is not None" name="calibre:series_index" content="${mi.format_series_index()}"/>
<meta py:if="mi.rating is not None" name="calibre:rating" content="${mi.rating}"/> <meta py:if="mi.rating is not None" name="calibre:rating" content="${mi.rating}"/>
<meta py:if="mi.timestamp is not None" name="calibre:timestamp" content="${mi.timestamp.isoformat()}"/>
<py:for each="tag in mi.tags"> <py:for each="tag in mi.tags">
<dc:subject py:if="mi.tags is not None">${tag}</dc:subject> <dc:subject py:if="mi.tags is not None">${tag}</dc:subject>
</py:for> </py:for>

View File

@ -442,9 +442,10 @@ class OPF(object):
comments = MetadataField('description') comments = MetadataField('description')
category = MetadataField('category') category = MetadataField('category')
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=float, none_is=1)
rating = MetadataField('rating', is_dc=False, formatter=int) rating = MetadataField('rating', is_dc=False, formatter=int)
timestamp = MetadataField('date', formatter=parser.parse) pubdate = MetadataField('date', formatter=parser.parse)
timestamp = MetadataField('timestamp', is_dc=False, formatter=parser.parse)
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True): def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True):

View File

@ -65,7 +65,7 @@ class Jacket(object):
comments = comments.replace('\r\n', '\n').replace('\n\n', '<br/><br/>') comments = comments.replace('\r\n', '\n').replace('\n\n', '<br/><br/>')
series = '<b>Series: </b>' + mi.series if mi.series else '' series = '<b>Series: </b>' + mi.series if mi.series else ''
if series and mi.series_index is not None: if series and mi.series_index is not None:
series += ' [%s]'%mi.series_index series += ' [%s]'%mi.format_series_index()
tags = mi.tags tags = mi.tags
if not tags: if not tags:
try: try:

View File

@ -58,7 +58,7 @@ class MergeMetadata(object):
m.add('creator', mi.book_producer, role='bkp') m.add('creator', mi.book_producer, role='bkp')
if mi.series_index is not None: if mi.series_index is not None:
m.clear('series_index') m.clear('series_index')
m.add('series_index', '%.2f'%mi.series_index) m.add('series_index', mi.format_series_index())
if mi.rating is not None: if mi.rating is not None:
m.clear('rating') m.clear('rating')
m.add('rating', '%.2f'%mi.rating) m.add('rating', '%.2f'%mi.rating)

View File

@ -19,7 +19,8 @@ from calibre.ebooks.metadata import MetaInformation
NONE = QVariant() #: Null value to return from the data function of item models NONE = QVariant() #: Null value to return from the data function of item models
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series'] ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher',
'tags', 'series', 'pubdate']
def _config(): def _config():
c = Config('gui', 'preferences for the calibre GUI') c = Config('gui', 'preferences for the calibre GUI')

View File

@ -119,7 +119,8 @@ class Widget(QWidget):
elif isinstance(g, XPathEdit): elif isinstance(g, XPathEdit):
g.edit.setText(val if val else '') g.edit.setText(val if val else '')
else: else:
raise Exception('Can\'t set value %s in %s'%(repr(val), type(g))) raise Exception('Can\'t set value %s in %s'%(repr(val),
unicode(g.objectName())))
self.post_set_value(g, val) self.post_set_value(g, val)
def set_help(self, msg): def set_help(self, msg):

View File

@ -83,7 +83,7 @@ class MetadataWidget(Widget, Ui_Form):
comments = unicode(self.comment.toPlainText()).strip() comments = unicode(self.comment.toPlainText()).strip()
if comments: if comments:
mi.comments = comments mi.comments = comments
mi.series_index = int(self.series_index.value()) mi.series_index = float(self.series_index.value())
if self.series.currentIndex() > -1: if self.series.currentIndex() > -1:
mi.series = unicode(self.series.currentText()).strip() mi.series = unicode(self.series.currentText()).strip()
tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')] tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')]

View File

@ -1,7 +1,8 @@
<ui version="4.0" > <?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class> <class>Form</class>
<widget class="QWidget" name="Form" > <widget class="QWidget" name="Form">
<property name="geometry" > <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
@ -9,59 +10,89 @@
<height>500</height> <height>500</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QHBoxLayout" name="horizontalLayout" > <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QGroupBox" name="groupBox_4" > <widget class="QGroupBox" name="groupBox_4">
<property name="title" > <property name="title">
<string>Book Cover</string> <string>Book Cover</string>
</property> </property>
<layout class="QGridLayout" name="_2" > <layout class="QGridLayout" name="_2">
<item row="1" column="0" > <item row="0" column="0">
<layout class="QVBoxLayout" name="_4" > <layout class="QHBoxLayout" name="_3">
<property name="spacing" > <item>
<widget class="ImageView" name="cover">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../../../../calibre/gui2/images.qrc">:/images/book.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_prefer_metadata_cover">
<property name="text">
<string>Use cover from &amp;source file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number> <number>6</number>
</property> </property>
<property name="margin" > <property name="margin">
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="label_5" > <widget class="QLabel" name="label_5">
<property name="text" > <property name="text">
<string>Change &amp;cover image:</string> <string>Change &amp;cover image:</string>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>cover_path</cstring> <cstring>cover_path</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="_5" > <layout class="QHBoxLayout" name="_5">
<property name="spacing" > <property name="spacing">
<number>6</number> <number>6</number>
</property> </property>
<property name="margin" > <property name="margin">
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QLineEdit" name="cover_path" > <widget class="QLineEdit" name="cover_path">
<property name="readOnly" > <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QToolButton" name="cover_button" > <widget class="QToolButton" name="cover_button">
<property name="toolTip" > <property name="toolTip">
<string>Browse for an image to use as the cover of this book.</string> <string>Browse for an image to use as the cover of this book.</string>
</property> </property>
<property name="text" > <property name="text">
<string>...</string> <string>...</string>
</property> </property>
<property name="icon" > <property name="icon">
<iconset resource="../../../../../../calibre/gui2/images.qrc" > <iconset resource="../../../../../../calibre/gui2/images.qrc">
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset> <normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property> </property>
</widget> </widget>
@ -70,243 +101,204 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../../../../../../calibre/gui2/images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
<zorder>opt_prefer_metadata_cover</zorder> <zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder> <zorder></zorder>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_2" > <layout class="QVBoxLayout" name="verticalLayout_2">
<item> <item>
<layout class="QGridLayout" name="_7" > <layout class="QGridLayout" name="_7">
<item row="0" column="0" > <item row="0" column="0">
<widget class="QLabel" name="label" > <widget class="QLabel" name="label">
<property name="text" > <property name="text">
<string>&amp;Title: </string> <string>&amp;Title: </string>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>title</cstring> <cstring>title</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1" > <item row="0" column="1">
<widget class="QLineEdit" name="title" > <widget class="QLineEdit" name="title">
<property name="toolTip" > <property name="toolTip">
<string>Change the title of this book</string> <string>Change the title of this book</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" > <item row="1" column="0">
<widget class="QLabel" name="label_2" > <widget class="QLabel" name="label_2">
<property name="text" > <property name="text">
<string>&amp;Author(s): </string> <string>&amp;Author(s): </string>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>author</cstring> <cstring>author</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1" > <item row="1" column="1">
<widget class="QLineEdit" name="author" > <widget class="QLineEdit" name="author">
<property name="sizePolicy" > <property name="sizePolicy">
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" > <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch> <horstretch>1</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="toolTip" > <property name="toolTip">
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string> <string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" > <item row="2" column="0">
<widget class="QLabel" name="label_6" > <widget class="QLabel" name="label_6">
<property name="text" > <property name="text">
<string>Author So&amp;rt:</string> <string>Author So&amp;rt:</string>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>author_sort</cstring> <cstring>author_sort</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1" > <item row="2" column="1">
<widget class="QLineEdit" name="author_sort" > <widget class="QLineEdit" name="author_sort">
<property name="sizePolicy" > <property name="sizePolicy">
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" > <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="toolTip" > <property name="toolTip">
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string> <string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" > <item row="3" column="0">
<widget class="QLabel" name="label_3" > <widget class="QLabel" name="label_3">
<property name="text" > <property name="text">
<string>&amp;Publisher: </string> <string>&amp;Publisher: </string>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>publisher</cstring> <cstring>publisher</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" > <item row="3" column="1">
<widget class="QLineEdit" name="publisher" > <widget class="QLineEdit" name="publisher">
<property name="toolTip" > <property name="toolTip">
<string>Change the publisher of this book</string> <string>Change the publisher of this book</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0" > <item row="4" column="0">
<widget class="QLabel" name="label_4" > <widget class="QLabel" name="label_4">
<property name="text" > <property name="text">
<string>Ta&amp;gs: </string> <string>Ta&amp;gs: </string>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>tags</cstring> <cstring>tags</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1" > <item row="4" column="1">
<widget class="QLineEdit" name="tags" > <widget class="QLineEdit" name="tags">
<property name="toolTip" > <property name="toolTip">
<string>Tags categorize the book. This is particularly useful while searching. &lt;br>&lt;br>They can be any words or phrases, separated by commas.</string> <string>Tags categorize the book. This is particularly useful while searching. &lt;br&gt;&lt;br&gt;They can be any words or phrases, separated by commas.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0" > <item row="5" column="0">
<widget class="QLabel" name="label_7" > <widget class="QLabel" name="label_7">
<property name="text" > <property name="text">
<string>&amp;Series:</string> <string>&amp;Series:</string>
</property> </property>
<property name="textFormat" > <property name="textFormat">
<enum>Qt::PlainText</enum> <enum>Qt::PlainText</enum>
</property> </property>
<property name="alignment" > <property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property> </property>
<property name="buddy" > <property name="buddy">
<cstring>series</cstring> <cstring>series</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1" > <item row="5" column="1">
<widget class="QComboBox" name="series" > <widget class="QComboBox" name="series">
<property name="sizePolicy" > <property name="sizePolicy">
<sizepolicy vsizetype="Fixed" hsizetype="Preferred" > <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>10</horstretch> <horstretch>10</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="toolTip" > <property name="toolTip">
<string>List of known series. You can add new series.</string> <string>List of known series. You can add new series.</string>
</property> </property>
<property name="whatsThis" > <property name="whatsThis">
<string>List of known series. You can add new series.</string> <string>List of known series. You can add new series.</string>
</property> </property>
<property name="editable" > <property name="editable">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="insertPolicy" > <property name="insertPolicy">
<enum>QComboBox::InsertAlphabetically</enum> <enum>QComboBox::InsertAlphabetically</enum>
</property> </property>
<property name="sizeAdjustPolicy" > <property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum> <enum>QComboBox::AdjustToContents</enum>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1" > <item row="6" column="1">
<widget class="QSpinBox" name="series_index" > <widget class="QDoubleSpinBox" name="series_index">
<property name="enabled" > <property name="prefix">
<bool>true</bool>
</property>
<property name="toolTip" >
<string>Series index.</string>
</property>
<property name="whatsThis" >
<string>Series index.</string>
</property>
<property name="prefix" >
<string>Book </string> <string>Book </string>
</property> </property>
<property name="minimum" > <property name="maximum">
<number>1</number> <double>9999.989999999999782</double>
</property> </property>
<property name="maximum" > <property name="value">
<number>10000</number> <double>1.000000000000000</double>
</property> </property>
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox_2" > <widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy" > <property name="sizePolicy">
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" > <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="maximumSize" > <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>16777215</width>
<height>200</height> <height>200</height>
</size> </size>
</property> </property>
<property name="title" > <property name="title">
<string>Comments</string> <string>Comments</string>
</property> </property>
<layout class="QGridLayout" name="_8" > <layout class="QGridLayout" name="_8">
<item row="0" column="0" > <item row="0" column="0">
<widget class="QTextEdit" name="comment" > <widget class="QTextEdit" name="comment">
<property name="maximumSize" > <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>16777215</width>
<height>180</height> <height>180</height>
@ -329,8 +321,8 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<resources> <resources>
<include location="../../../../../../calibre/gui2/images.qrc" /> <include location="../../../../../../calibre/gui2/images.qrc"/>
<include location="../images.qrc" /> <include location="../images.qrc"/>
</resources> </resources>
<connections/> <connections/>
</ui> </ui>

View File

@ -177,7 +177,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1" colspan="2"> <item row="3" column="1">
<widget class="QSpinBox" name="rating"> <widget class="QSpinBox" name="rating">
<property name="toolTip"> <property name="toolTip">
<string>Rating of this book. 0-5 stars</string> <string>Rating of this book. 0-5 stars</string>
@ -309,28 +309,6 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="7" column="1" colspan="2">
<widget class="QSpinBox" name="series_index">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Series index.</string>
</property>
<property name="whatsThis">
<string>Series index.</string>
</property>
<property name="prefix">
<string>Book </string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="8" column="0"> <item row="8" column="0">
<widget class="QLabel" name="label_9"> <widget class="QLabel" name="label_9">
<property name="text"> <property name="text">
@ -357,6 +335,19 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="authors"/> <widget class="QLineEdit" name="authors"/>
</item> </item>
<item row="7" column="1">
<widget class="QDoubleSpinBox" name="series_index">
<property name="enabled">
<bool>false</bool>
</property>
<property name="prefix">
<string>Book </string>
</property>
<property name="maximum">
<double>9999.989999999999782</double>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -640,7 +631,6 @@
<tabstop>series</tabstop> <tabstop>series</tabstop>
<tabstop>tag_editor_button</tabstop> <tabstop>tag_editor_button</tabstop>
<tabstop>remove_series_button</tabstop> <tabstop>remove_series_button</tabstop>
<tabstop>series_index</tabstop>
<tabstop>isbn</tabstop> <tabstop>isbn</tabstop>
<tabstop>comments</tabstop> <tabstop>comments</tabstop>
<tabstop>fetch_metadata_button</tabstop> <tabstop>fetch_metadata_button</tabstop>

View File

@ -20,7 +20,7 @@ from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog error_dialog
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.ebooks.metadata import string_to_authors from calibre.ebooks.metadata import string_to_authors, fmt_sidx
class LibraryDelegate(QItemDelegate): class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
@ -98,40 +98,38 @@ class DateDelegate(QStyledItemDelegate):
qde.setCalendarPopup(True) qde.setCalendarPopup(True)
return qde return qde
class BooksModel(QAbstractTableModel): class PubDateDelegate(QStyledItemDelegate):
coding = zip(
[1000,900,500,400,100,90,50,40,10,9,5,4,1],
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
)
def displayText(self, val, locale):
return val.toDate().toString('MMM yyyy')
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('MM yyyy')
qde.setMinimumDate(QDate(101,1,1))
qde.setCalendarPopup(True)
return qde
class BooksModel(QAbstractTableModel):
headers = { headers = {
'title' : _("Title"), 'title' : _("Title"),
'authors' : _("Author(s)"), 'authors' : _("Author(s)"),
'size' : _("Size (MB)"), 'size' : _("Size (MB)"),
'timestamp' : _("Date"), 'timestamp' : _("Date"),
'pubdate' : _('Published'),
'rating' : _('Rating'), 'rating' : _('Rating'),
'publisher' : _("Publisher"), 'publisher' : _("Publisher"),
'tags' : _("Tags"), 'tags' : _("Tags"),
'series' : _("Series"), 'series' : _("Series"),
} }
@classmethod
def roman(cls, num):
if num <= 0 or num >= 4000 or int(num) != num:
return str(num)
result = []
for d, r in cls.coding:
while num >= d:
result.append(r)
num -= d
return ''.join(result)
def __init__(self, parent=None, buffer=40): def __init__(self, parent=None, buffer=40):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.column_map = config['column_map'] self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher', self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp'] 'tags', 'series', 'timestamp', 'pubdate']
self.default_image = QImage(':/images/book.svg') self.default_image = QImage(':/images/book.svg')
self.sorted_on = ('timestamp', Qt.AscendingOrder) self.sorted_on = ('timestamp', Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model self.last_search = '' # The last search performed on this model
@ -157,8 +155,12 @@ class BooksModel(QAbstractTableModel):
tidx = self.column_map.index('timestamp') tidx = self.column_map.index('timestamp')
except ValueError: except ValueError:
tidx = -1 tidx = -1
try:
pidx = self.column_map.index('pubdate')
except ValueError:
pidx = -1
self.emit(SIGNAL('columns_sorted(int,int)'), idx, tidx) self.emit(SIGNAL('columns_sorted(int,int,int)'), idx, tidx, pidx)
def set_database(self, db): def set_database(self, db):
@ -186,8 +188,8 @@ class BooksModel(QAbstractTableModel):
self.db = None self.db = None
self.reset() self.reset()
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False): def add_books(self, paths, formats, metadata, add_duplicates=False):
ret = self.db.add_books(paths, formats, metadata, uris, ret = self.db.add_books(paths, formats, metadata,
add_duplicates=add_duplicates) add_duplicates=add_duplicates)
self.count_changed() self.count_changed()
return ret return ret
@ -313,7 +315,7 @@ class BooksModel(QAbstractTableModel):
series = self.db.series(idx) series = self.db.series(idx)
if series: if series:
sidx = self.db.series_index(idx) sidx = self.db.series_index(idx)
sidx = self.__class__.roman(sidx) if self.use_roman_numbers else str(sidx) sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series) data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
return data return data
@ -492,6 +494,7 @@ class BooksModel(QAbstractTableModel):
ridx = FIELD_MAP['rating'] ridx = FIELD_MAP['rating']
pidx = FIELD_MAP['publisher'] pidx = FIELD_MAP['publisher']
tmdx = FIELD_MAP['timestamp'] tmdx = FIELD_MAP['timestamp']
pddx = FIELD_MAP['pubdate']
srdx = FIELD_MAP['series'] srdx = FIELD_MAP['series']
tgdx = FIELD_MAP['tags'] tgdx = FIELD_MAP['tags']
siix = FIELD_MAP['series_index'] siix = FIELD_MAP['series_index']
@ -508,6 +511,12 @@ class BooksModel(QAbstractTableModel):
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight) dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QDate(dt.year, dt.month, dt.day) return QDate(dt.year, dt.month, dt.day)
def pubdate(r):
dt = self.db.data[r][pddx]
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QDate(dt.year, dt.month, dt.day)
def rating(r): def rating(r):
r = self.db.data[r][ridx] r = self.db.data[r][ridx]
r = r/2 if r else 0 r = r/2 if r else 0
@ -526,8 +535,8 @@ class BooksModel(QAbstractTableModel):
def series(r): def series(r):
series = self.db.data[r][srdx] series = self.db.data[r][srdx]
if series: if series:
return series + ' [%d]'%self.db.data[r][siix] idx = fmt_sidx(self.db.data[r][siix])
return series + ' [%s]'%idx
def size(r): def size(r):
size = self.db.data[r][sidx] size = self.db.data[r][sidx]
if size: if size:
@ -538,6 +547,7 @@ class BooksModel(QAbstractTableModel):
'authors' : authors, 'authors' : authors,
'size' : size, 'size' : size,
'timestamp': timestamp, 'timestamp': timestamp,
'pubdate' : pubdate,
'rating' : rating, 'rating' : rating,
'publisher': publisher, 'publisher': publisher,
'tags' : tags, 'tags' : tags,
@ -577,7 +587,7 @@ class BooksModel(QAbstractTableModel):
if column not in self.editable_cols: if column not in self.editable_cols:
return False return False
val = int(value.toInt()[0]) if column == 'rating' else \ val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column == 'timestamp' else \ value.toDate() if column in ('timestamp', 'pubdate') else \
unicode(value.toString()) unicode(value.toString())
id = self.db.id(row) id = self.db.id(row)
if column == 'rating': if column == 'rating':
@ -585,10 +595,10 @@ class BooksModel(QAbstractTableModel):
val *= 2 val *= 2
self.db.set_rating(id, val) self.db.set_rating(id, val)
elif column == 'series': elif column == 'series':
pat = re.compile(r'\[(\d+)\]') pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(val) match = pat.search(val)
if match is not None: if match is not None:
self.db.set_series_index(id, int(match.group(1))) self.db.set_series_index(id, float(match.group(1)))
val = pat.sub('', val) val = pat.sub('', val)
val = val.strip() val = val.strip()
if val: if val:
@ -598,6 +608,11 @@ class BooksModel(QAbstractTableModel):
return False return False
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight) dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
self.db.set_timestamp(id, dt) self.db.set_timestamp(id, dt)
elif column == 'pubdate':
if val.isNull() or not val.isValid():
return False
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
self.db.set_pubdate(id, dt)
else: else:
self.db.set(row, column, val) self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
@ -625,29 +640,35 @@ class BooksView(TableView):
TableView.__init__(self, parent) TableView.__init__(self, parent)
self.rating_delegate = LibraryDelegate(self) self.rating_delegate = LibraryDelegate(self)
self.timestamp_delegate = DateDelegate(self) self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
self.display_parent = parent self.display_parent = parent
self._model = modelcls(self) self._model = modelcls(self)
self.setModel(self._model) self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) self.setSortingEnabled(True)
try: try:
self.columns_sorted(self._model.column_map.index('rating'), cm = self._model.column_map
self._model.column_map.index('timestamp')) self.columns_sorted(cm.index('rating') if 'rating' in cm else -1,
cm.index('timestamp') if 'timestamp' in cm else -1,
cm.index('pubdate') if 'pubdate' in cm else -1)
except ValueError: except ValueError:
pass pass
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed) self._model.current_changed)
self.connect(self._model, SIGNAL('columns_sorted(int, int)'), self.columns_sorted, Qt.QueuedConnection) self.connect(self._model, SIGNAL('columns_sorted(int,int,int)'),
self.columns_sorted, Qt.QueuedConnection)
def columns_sorted(self, rating_col, timestamp_col): def columns_sorted(self, rating_col, timestamp_col, pubdate_col):
for i in range(self.model().columnCount(None)): for i in range(self.model().columnCount(None)):
if self.itemDelegateForColumn(i) in (self.rating_delegate, if self.itemDelegateForColumn(i) in (self.rating_delegate,
self.timestamp_delegate): self.timestamp_delegate, self.pubdate_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate()) self.setItemDelegateForColumn(i, self.itemDelegate())
if rating_col > -1: if rating_col > -1:
self.setItemDelegateForColumn(rating_col, self.rating_delegate) self.setItemDelegateForColumn(rating_col, self.rating_delegate)
if timestamp_col > -1: if timestamp_col > -1:
self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate) self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate)
if pubdate_col > -1:
self.setItemDelegateForColumn(pubdate_col, self.pubdate_delegate)
def set_context_menu(self, edit_metadata, send_to_device, convert, view, def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, similar_menu=None): save, open_folder, book_details, similar_menu=None):

View File

@ -27,7 +27,7 @@ class Concatenate(object):
return self.ans[:-len(self.sep)] return self.ans[:-len(self.sep)]
return self.ans return self.ans
class Connection(sqlite.Connection): class Connection(sqlite.Connection):
def get(self, *args, **kw): def get(self, *args, **kw):
ans = self.execute(*args) ans = self.execute(*args)
if not kw.get('all', True): if not kw.get('all', True):
@ -785,8 +785,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
FROM books; FROM books;
''') ''')
conn.execute('pragma user_version=12') conn.execute('pragma user_version=12')
conn.commit() conn.commit()
def __init__(self, dbpath, row_factory=False): def __init__(self, dbpath, row_factory=False):
self.dbpath = dbpath self.dbpath = dbpath
self.conn = _connect(dbpath) self.conn = _connect(dbpath)
@ -901,7 +901,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def id(self, index): def id(self, index):
return self.data[index][0] return self.data[index][0]
def row(self, id): def row(self, id):
for r, record in enumerate(self.data): for r, record in enumerate(self.data):
if record[0] == id: if record[0] == id:
@ -916,8 +916,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return _('Unknown') return _('Unknown')
def authors(self, index, index_is_id=False): def authors(self, index, index_is_id=False):
''' '''
Authors as a comma separated list or None. Authors as a comma separated list or None.
In the comma separated list, commas in author names are replaced by | symbols In the comma separated list, commas in author names are replaced by | symbols
''' '''
if not index_is_id: if not index_is_id:
@ -939,11 +939,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
if index_is_id: if index_is_id:
return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False) return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False)
return self.data[index][3] return self.data[index][3]
def publisher_id(self, index, index_is_id=False): def publisher_id(self, index, index_is_id=False):
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
return self.conn.get('SELECT publisher from books_publishers_link WHERE book=?', (id,), all=False) return self.conn.get('SELECT publisher from books_publishers_link WHERE book=?', (id,), all=False)
def rating(self, index, index_is_id=False): def rating(self, index, index_is_id=False):
if index_is_id: if index_is_id:
return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False) return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False)
@ -983,7 +983,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def series(self, index, index_is_id=False): def series(self, index, index_is_id=False):
id = self.series_id(index, index_is_id) id = self.series_id(index, index_is_id)
return self.conn.get('SELECT name from series WHERE id=?', (id,), all=False) return self.conn.get('SELECT name from series WHERE id=?', (id,), all=False)
def series_index(self, index, index_is_id=False): def series_index(self, index, index_is_id=False):
ans = None ans = None
if not index_is_id: if not index_is_id:
@ -991,9 +991,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
else: else:
ans = self.conn.get('SELECT series_index FROM books WHERE id=?', (index,), all=False) ans = self.conn.get('SELECT series_index FROM books WHERE id=?', (index,), all=False)
try: try:
return int(ans) return float(ans)
except: except:
return 1 return 1.0
def books_in_series(self, series_id): def books_in_series(self, series_id):
''' '''
@ -1021,7 +1021,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
'''Comments as string or None''' '''Comments as string or None'''
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
return self.conn.get('SELECT text FROM comments WHERE book=?', (id,), all=False) return self.conn.get('SELECT text FROM comments WHERE book=?', (id,), all=False)
def formats(self, index, index_is_id=False): def formats(self, index, index_is_id=False):
''' Return available formats as a comma separated list ''' ''' Return available formats as a comma separated list '''
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
@ -1041,11 +1041,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def all_series(self): def all_series(self):
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM series')] self.conn.get('SELECT id, name FROM series')]
def all_authors(self): def all_authors(self):
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM authors')] self.conn.get('SELECT id, name FROM authors')]
def all_publishers(self): def all_publishers(self):
return [ (i[0], i[1]) for i in \ return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM publishers')] self.conn.get('SELECT id, name FROM publishers')]
@ -1278,9 +1278,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
self.set_series(id, mi.series) self.set_series(id, mi.series)
if mi.cover_data[1] is not None: if mi.cover_data[1] is not None:
self.set_cover(id, mi.cover_data[1]) self.set_cover(id, mi.cover_data[1])
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
''' '''
@ -1385,16 +1385,16 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def all_ids(self): def all_ids(self):
return [i[0] for i in self.conn.get('SELECT id FROM books')] return [i[0] for i in self.conn.get('SELECT id FROM books')]
def has_id(self, id): def has_id(self, id):
return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None
class SearchToken(object): class SearchToken(object):
@ -1455,4 +1455,4 @@ def text_to_tokens(text):
if __name__ == '__main__': if __name__ == '__main__':
sqlite.enable_callback_tracebacks(True) sqlite.enable_callback_tracebacks(True)
db = LibraryDatabase('/home/kovid/temp/library1.db.orig') db = LibraryDatabase('/home/kovid/temp/library1.db.orig')

View File

@ -50,7 +50,8 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
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,
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15} 'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
'lccn':16, 'pubdate':17, 'flags':18}
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys())) INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
@ -472,6 +473,53 @@ class LibraryDatabase2(LibraryDatabase):
FROM books; FROM books;
''') ''')
def upgrade_version_4(self):
'Rationalize books table'
self.conn.executescript('''
BEGIN TRANSACTION;
CREATE TEMPORARY TABLE
books_backup(id,title,sort,timestamp,series_index,author_sort,isbn,path);
INSERT INTO books_backup SELECT id,title,sort,timestamp,series_index,author_sort,isbn,path FROM books;
DROP TABLE books;
CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT 'Unknown' COLLATE NOCASE,
sort TEXT COLLATE NOCASE,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
pubdate TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
series_index REAL NOT NULL DEFAULT 1.0,
author_sort TEXT COLLATE NOCASE,
isbn TEXT DEFAULT "" COLLATE NOCASE,
lccn TEXT DEFAULT "" COLLATE NOCASE,
path TEXT NOT NULL DEFAULT "",
flags INTEGER NOT NULL DEFAULT 1
);
INSERT INTO
books (id,title,sort,timestamp,pubdate,series_index,author_sort,isbn,path)
SELECT id,title,sort,timestamp,timestamp,series_index,author_sort,isbn,path FROM books_backup;
DROP TABLE books_backup;
DROP VIEW meta;
CREATE VIEW meta AS
SELECT id, title,
(SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link WHERE book=books.id)) authors,
(SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher,
(SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating,
timestamp,
(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size,
(SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags,
(SELECT text FROM comments WHERE book=books.id) comments,
(SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series,
series_index,
sort,
author_sort,
(SELECT concat(format) FROM data WHERE data.book=books.id) formats,
isbn,
path,
lccn,
pubdate,
flags
FROM books;
''')
def last_modified(self): def last_modified(self):
''' Return last modified time as a UTC datetime object''' ''' Return last modified time as a UTC datetime object'''
@ -610,6 +658,16 @@ class LibraryDatabase2(LibraryDatabase):
return img return img
return f if as_file else f.read() return f if as_file else f.read()
def timestamp(self, index, index_is_id=False):
if index_is_id:
return self.conn.get('SELECT timestamp FROM meta WHERE id=?', (index,), all=False)
return self.data[index][FIELD_MAP['timestamp']]
def pubdate(self, index, index_is_id=False):
if index_is_id:
return self.conn.get('SELECT pubdate FROM meta WHERE id=?', (index,), all=False)
return self.data[index][FIELD_MAP['pubdate']]
def get_metadata(self, idx, index_is_id=False, get_cover=False): def get_metadata(self, idx, index_is_id=False, get_cover=False):
''' '''
Convenience method to return metadata as a L{MetaInformation} object. Convenience method to return metadata as a L{MetaInformation} object.
@ -621,6 +679,7 @@ class LibraryDatabase2(LibraryDatabase):
mi.comments = self.comments(idx, index_is_id=index_is_id) mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id) mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id) mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
tags = self.tags(idx, index_is_id=index_is_id) tags = self.tags(idx, index_is_id=index_is_id)
if tags: if tags:
mi.tags = [i.strip() for i in tags.split(',')] mi.tags = [i.strip() for i in tags.split(',')]
@ -917,7 +976,7 @@ class LibraryDatabase2(LibraryDatabase):
self.set_comment(id, mi.comments, notify=False) self.set_comment(id, mi.comments, notify=False)
if mi.isbn and mi.isbn.strip(): if mi.isbn and mi.isbn.strip():
self.set_isbn(id, mi.isbn, notify=False) self.set_isbn(id, mi.isbn, notify=False)
if mi.series_index and mi.series_index > 0: if mi.series_index:
self.set_series_index(id, mi.series_index, notify=False) self.set_series_index(id, mi.series_index, notify=False)
if getattr(mi, 'timestamp', None) is not None: if getattr(mi, 'timestamp', None) is not None:
self.set_timestamp(id, mi.timestamp, notify=False) self.set_timestamp(id, mi.timestamp, notify=False)
@ -983,6 +1042,15 @@ class LibraryDatabase2(LibraryDatabase):
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
def set_pubdate(self, id, dt, notify=True):
if dt:
self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id))
self.data.set(id, FIELD_MAP['pubdate'], dt, row_is_id=True)
self.conn.commit()
if notify:
self.notify('metadata', [id])
def set_publisher(self, id, publisher, notify=True): def set_publisher(self, id, publisher, notify=True):
self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1') self.conn.execute('DELETE FROM publishers WHERE (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) < 1')
@ -1103,17 +1171,11 @@ class LibraryDatabase2(LibraryDatabase):
def set_series_index(self, id, idx, notify=True): def set_series_index(self, id, idx, notify=True):
if idx is None: if idx is None:
idx = 1 idx = 1.0
idx = int(idx) idx = float(idx)
self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id)) self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (idx, id))
self.conn.commit() self.conn.commit()
try: self.data.set(id, FIELD_MAP['series_index'], idx, row_is_id=True)
row = self.row(id)
if row is not None:
self.data.set(row, 10, idx)
except ValueError:
pass
self.data.set(id, FIELD_MAP['series_index'], int(idx), row_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])
@ -1156,7 +1218,7 @@ class LibraryDatabase2(LibraryDatabase):
stream.seek(0) stream.seek(0)
mi = get_metadata(stream, format, use_libprs_metadata=False) mi = get_metadata(stream, format, use_libprs_metadata=False)
stream.seek(0) stream.seek(0)
mi.series_index = 1 mi.series_index = 1.0
mi.tags = [_('News'), recipe.title] mi.tags = [_('News'), recipe.title]
obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)', obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)',
(mi.title, mi.authors[0])) (mi.title, mi.authors[0]))
@ -1188,7 +1250,7 @@ class LibraryDatabase2(LibraryDatabase):
def create_book_entry(self, mi, cover=None, add_duplicates=True): def create_book_entry(self, mi, cover=None, add_duplicates=True):
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
return None return None
series_index = 1 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
title = mi.title title = mi.title
if isinstance(aus, str): if isinstance(aus, str):
@ -1207,33 +1269,29 @@ class LibraryDatabase2(LibraryDatabase):
return id return id
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): def add_books(self, paths, formats, metadata, add_duplicates=True):
''' '''
Add a book to the database. The result cache is not updated. Add a book to the database. The result cache is not updated.
:param:`paths` List of paths to book files or file-like objects :param:`paths` List of paths to book files or file-like objects
''' '''
formats, metadata, uris = iter(formats), iter(metadata), iter(uris) formats, metadata = iter(formats), iter(metadata)
duplicates = [] duplicates = []
ids = [] ids = []
for path in paths: for path in paths:
mi = metadata.next() mi = metadata.next()
format = formats.next() format = formats.next()
try:
uri = uris.next()
except StopIteration:
uri = None
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
duplicates.append((path, format, mi, uri)) duplicates.append((path, format, mi))
continue continue
series_index = 1 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
title = mi.title title = mi.title
if isinstance(aus, str): if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace') aus = aus.decode(preferred_encoding, 'replace')
if isinstance(title, str): if isinstance(title, str):
title = title.decode(preferred_encoding) title = title.decode(preferred_encoding)
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, uri, series_index, aus)) (title, series_index, aus))
id = obj.lastrowid id = obj.lastrowid
self.data.books_added([id], self.conn) self.data.books_added([id], self.conn)
ids.append(id) ids.append(id)
@ -1251,12 +1309,11 @@ class LibraryDatabase2(LibraryDatabase):
paths = list(duplicate[0] for duplicate in duplicates) paths = list(duplicate[0] for duplicate in duplicates)
formats = list(duplicate[1] for duplicate in duplicates) formats = list(duplicate[1] for duplicate in duplicates)
metadata = list(duplicate[2] for duplicate in duplicates) metadata = list(duplicate[2] for duplicate in duplicates)
uris = list(duplicate[3] for duplicate in duplicates) return (paths, formats, metadata), len(ids)
return (paths, formats, metadata, uris), len(ids)
return None, len(ids) return None, len(ids)
def import_book(self, mi, formats, notify=True): def import_book(self, mi, formats, notify=True):
series_index = 1 if mi.series_index is None else mi.series_index series_index = 1.0 if mi.series_index is None else mi.series_index
if not mi.title: if not mi.title:
mi.title = _('Unknown') mi.title = _('Unknown')
if not mi.authors: if not mi.authors:
@ -1266,8 +1323,8 @@ class LibraryDatabase2(LibraryDatabase):
aus = aus.decode(preferred_encoding, 'replace') aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \ title = mi.title if isinstance(mi.title, unicode) else \
mi.title.decode(preferred_encoding, 'replace') mi.title.decode(preferred_encoding, 'replace')
obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
(title, None, series_index, aus)) (title, series_index, aus))
id = obj.lastrowid id = obj.lastrowid
self.data.books_added([id], self.conn) self.data.books_added([id], self.conn)
self.set_path(id, True) self.set_path(id, True)
@ -1368,12 +1425,12 @@ class LibraryDatabase2(LibraryDatabase):
QCoreApplication.processEvents() QCoreApplication.processEvents()
db.conn.row_factory = lambda cursor, row : tuple(row) db.conn.row_factory = lambda cursor, row : tuple(row)
db.conn.text_factory = lambda x : unicode(x, 'utf-8', 'replace') db.conn.text_factory = lambda x : unicode(x, 'utf-8', 'replace')
books = db.conn.get('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC') books = db.conn.get('SELECT id, title, sort, timestamp, series_index, author_sort, isbn FROM books ORDER BY id ASC')
progress.setAutoReset(False) progress.setAutoReset(False)
progress.setRange(0, len(books)) progress.setRange(0, len(books))
for book in books: for book in books:
self.conn.execute('INSERT INTO books(id, title, sort, timestamp, uri, series_index, author_sort, isbn) VALUES(?, ?, ?, ?, ?, ?, ?, ?);', book) self.conn.execute('INSERT INTO books(id, title, sort, timestamp, series_index, author_sort, isbn) VALUES(?, ?, ?, ?, ?, ?, ?, ?);', book)
tables = ''' tables = '''
authors ratings tags series books_tags_link authors ratings tags series books_tags_link

View File

@ -25,6 +25,7 @@ from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
from calibre.utils.config import config_dir from calibre.utils.config import config_dir
from calibre.utils.mdns import publish as publish_zeroconf, \ from calibre.utils.mdns import publish as publish_zeroconf, \
stop_server as stop_zeroconf stop_server as stop_zeroconf
from calibre.ebooks.metadata import fmt_sidx
build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S') build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
server_resources['jquery.js'] = jquery server_resources['jquery.js'] = jquery
@ -271,7 +272,7 @@ class LibraryServer(object):
@expose @expose
def stanza(self): def stanza(self):
' Feeds to read calibre books on a ipod with stanza.' 'Feeds to read calibre books on a ipod with stanza.'
books = [] books = []
for record in iter(self.db): for record in iter(self.db):
r = record[FIELD_MAP['formats']] r = record[FIELD_MAP['formats']]
@ -289,8 +290,8 @@ class LibraryServer(object):
extra.append('TAGS: %s<br />'%', '.join(tags.split(','))) extra.append('TAGS: %s<br />'%', '.join(tags.split(',')))
series = record[FIELD_MAP['series']] series = record[FIELD_MAP['series']]
if series: if series:
extra.append('SERIES: %s [%d]<br />'%(series, extra.append('SERIES: %s [%s]<br />'%(series,
record[FIELD_MAP['series_index']])) fmt_sidx(record[FIELD_MAP['series_index']])))
fmt = 'epub' if 'EPUB' in r else 'pdb' fmt = 'epub' if 'EPUB' in r else 'pdb'
mimetype = guess_type('dummy.'+fmt)[0] mimetype = guess_type('dummy.'+fmt)[0]
books.append(self.STANZA_ENTRY.generate( books.append(self.STANZA_ENTRY.generate(
@ -339,6 +340,7 @@ class LibraryServer(object):
for record in items[start:start+num]: for record in items[start:start+num]:
aus = record[2] if record[2] else __builtins__._('Unknown') aus = record[2] if record[2] else __builtins__._('Unknown')
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')]) authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
r[10] = fmt_sidx(r[10])
books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8'))
updated = self.db.last_modified() updated = self.db.last_modified()

1
todo
View File

@ -2,6 +2,7 @@
* Refactor web.fetch.simple to use per connection timeouts via the timeout kwarg for mechanize.open * Refactor web.fetch.simple to use per connection timeouts via the timeout kwarg for mechanize.open
* Rationalize books table. Add a pubdate column, remove the uri column (and associated support in add_books) and convert series_index to a float. * Rationalize books table. Add a pubdate column, remove the uri column (and associated support in add_books) and convert series_index to a float.
- test adding/recusrsize adding and adding of duplicates
* Testing framework * Testing framework