diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 0174bf1ec3..6d0d14f6c2 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -42,6 +42,31 @@ def title_sort(title):
title = title.replace(prep, '') + ', ' + prep
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):
'''
@@ -187,7 +212,8 @@ class MetaInformation(object):
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language',
- 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc'):
+ 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
+ 'pubdate'):
if hasattr(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',
'series', 'series_index', 'rating', 'isbn', 'language',
'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))
@@ -231,7 +257,7 @@ class MetaInformation(object):
'publisher', 'series', 'series_index', 'rating',
'isbn', 'application_id', 'manifest', 'spine', 'toc',
'cover', 'language', 'guide', 'book_producer',
- 'timestamp', 'lccn', 'lcc', 'ddc'):
+ 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate'):
if hasattr(mi, attr):
val = getattr(mi, attr)
if val is not None:
@@ -262,8 +288,8 @@ class MetaInformation(object):
try:
x = float(self.series_index)
except ValueError:
- x = 1.0
- return '%d'%x if int(x) == x else '%.2f'%x
+ x = 1
+ return fmt_sidx(x)
def authors_from_string(self, raw):
self.authors = string_to_authors(raw)
@@ -299,6 +325,8 @@ class MetaInformation(object):
fmt('Rating', self.rating)
if self.timestamp is not None:
fmt('Timestamp', self.timestamp.isoformat(' '))
+ if self.pubdate is not None:
+ fmt('Published', self.pubdate.isoformat(' '))
if self.lccn:
fmt('LCCN', unicode(self.lccn))
if self.lcc:
@@ -327,6 +355,8 @@ class MetaInformation(object):
ans += [(_('Language'), unicode(self.language))]
if self.timestamp is not None:
ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]
+ if self.pubdate is not None:
+ ans += [(_('Published'), unicode(self.pubdate.isoformat(' ')))]
for i, x in enumerate(ans):
ans[i] = u'
%s | %s |
'%x
return u''%u'\n'.join(ans)
diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py
index 5b85f935a4..e81f8fe108 100644
--- a/src/calibre/ebooks/metadata/fb2.py
+++ b/src/calibre/ebooks/metadata/fb2.py
@@ -5,7 +5,7 @@ __copyright__ = '2008, Anatoly Shipitsin '
'''Read meta information from fb2 files'''
-import sys, os, mimetypes
+import mimetypes
from base64 import b64decode
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
@@ -32,7 +32,7 @@ def get_metadata(stream):
if not exts:
exts = ['.jpg']
cdata = (exts[0][1:], b64decode(binary.string.strip()))
-
+
if comments:
comments = u''.join(comments.findAll(text=True))
series = soup.find("sequence")
@@ -42,7 +42,7 @@ def get_metadata(stream):
if series:
mi.series = series.get('name', None)
try:
- mi.series_index = int(series.get('number', None))
+ mi.series_index = float(series.get('number', None))
except (TypeError, ValueError):
pass
if cdata:
diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py
index 1460a97eee..9a11ea4fca 100644
--- a/src/calibre/ebooks/metadata/meta.py
+++ b/src/calibre/ebooks/metadata/meta.py
@@ -145,7 +145,7 @@ def metadata_from_filename(name, pat=None):
pass
try:
si = match.group('series_index')
- mi.series_index = int(si)
+ mi.series_index = float(si)
except (IndexError, ValueError, TypeError):
pass
try:
diff --git a/src/calibre/ebooks/metadata/opf.py b/src/calibre/ebooks/metadata/opf.py
index 94264a285e..f508ffd517 100644
--- a/src/calibre/ebooks/metadata/opf.py
+++ b/src/calibre/ebooks/metadata/opf.py
@@ -2,8 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
'''Read/Write metadata from Open Packaging Format (.opf) files.'''
-import sys, re, os, glob
-import cStringIO
+import re, os
import uuid
from urllib import unquote, quote
@@ -15,14 +14,14 @@ from calibre.ebooks.metadata import Resource, ResourceCollection
from calibre.ebooks.metadata.toc import TOC
class OPFSoup(BeautifulStoneSoup):
-
+
def __init__(self, raw):
- BeautifulStoneSoup.__init__(self, raw,
+ BeautifulStoneSoup.__init__(self, raw,
convertEntities=BeautifulSoup.HTML_ENTITIES,
selfClosingTags=['item', 'itemref', 'reference'])
class ManifestItem(Resource):
-
+
@staticmethod
def from_opf_manifest_item(item, basedir):
if item.has_key('href'):
@@ -37,7 +36,7 @@ class ManifestItem(Resource):
if mt:
res.mime_type = mt
return res
-
+
@dynamic_property
def media_type(self):
def fget(self):
@@ -45,28 +44,28 @@ class ManifestItem(Resource):
def fset(self, val):
self.mime_type = val
return property(fget=fget, fset=fset)
-
-
+
+
def __unicode__(self):
return u' '%(self.id, self.href(), self.media_type)
-
+
def __str__(self):
return unicode(self).encode('utf-8')
-
+
def __repr__(self):
return unicode(self)
-
-
+
+
def __getitem__(self, index):
if index == 0:
return self.href()
if index == 1:
return self.media_type
raise IndexError('%d out of bounds.'%index)
-
+
class Manifest(ResourceCollection):
-
+
@staticmethod
def from_opf_manifest_element(manifest, dir):
m = Manifest()
@@ -81,7 +80,7 @@ class Manifest(ResourceCollection):
except ValueError:
continue
return m
-
+
@staticmethod
def from_paths(entries):
'''
@@ -96,37 +95,37 @@ class Manifest(ResourceCollection):
m.next_id += 1
m.append(mi)
return m
-
+
def __init__(self):
ResourceCollection.__init__(self)
self.next_id = 1
-
-
+
+
def item(self, id):
for i in self:
if i.id == id:
return i
-
+
def id_for_path(self, path):
path = os.path.normpath(os.path.abspath(path))
for i in self:
if i.path and os.path.normpath(i.path) == path:
- return i.id
-
+ return i.id
+
def path_for_id(self, id):
for i in self:
if i.id == id:
return i.path
class Spine(ResourceCollection):
-
+
class Item(Resource):
-
+
def __init__(self, idfunc, *args, **kwargs):
Resource.__init__(self, *args, **kwargs)
self.is_linear = True
self.id = idfunc(self.path)
-
+
@staticmethod
def from_opf_spine_element(spine, manifest):
s = Spine(manifest)
@@ -137,7 +136,7 @@ class Spine(ResourceCollection):
r.is_linear = itemref.get('linear', 'yes') == 'yes'
s.append(r)
return s
-
+
@staticmethod
def from_paths(paths, manifest):
s = Spine(manifest)
@@ -147,14 +146,14 @@ class Spine(ResourceCollection):
except:
continue
return s
-
-
-
+
+
+
def __init__(self, manifest):
ResourceCollection.__init__(self)
self.manifest = manifest
-
-
+
+
def linear_items(self):
for r in self:
if r.is_linear:
@@ -164,16 +163,16 @@ class Spine(ResourceCollection):
for r in self:
if not r.is_linear:
yield r.path
-
+
def items(self):
for i in self:
yield i.path
-
-
+
+
class Guide(ResourceCollection):
-
+
class Reference(Resource):
-
+
@staticmethod
def from_opf_resource_item(ref, basedir):
title, href, type = ref.get('title', ''), ref['href'], ref['type']
@@ -181,14 +180,14 @@ class Guide(ResourceCollection):
res.title = title
res.type = type
return res
-
+
def __repr__(self):
ans = ''
-
-
+
+
@staticmethod
def from_opf_guide(guide_elem, base_dir=os.getcwdu()):
coll = Guide()
@@ -199,29 +198,29 @@ class Guide(ResourceCollection):
except:
continue
return coll
-
+
def set_cover(self, path):
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'):
self.append(Guide.Reference(path, is_path=True))
self[-1].type = type
self[-1].title = ''
-
+
class standard_field(object):
-
+
def __init__(self, name):
self.name = name
-
+
def __get__(self, obj, typ=None):
return getattr(obj, 'get_'+self.name)()
-
+
class OPF(MetaInformation):
-
+
MIMETYPE = 'application/oebps-package+xml'
ENTITY_PATTERN = re.compile(r'&(\S+?);')
-
+
uid = standard_field('uid')
application_id = standard_field('application_id')
title = standard_field('title')
@@ -238,29 +237,29 @@ class OPF(MetaInformation):
series_index = standard_field('series_index')
rating = standard_field('rating')
tags = standard_field('tags')
-
+
def __init__(self):
raise NotImplementedError('Abstract base class')
-
+
@dynamic_property
def package(self):
def fget(self):
return self.soup.find(re.compile('package'))
return property(fget=fget)
-
+
@dynamic_property
def metadata(self):
def fget(self):
return self.package.find(re.compile('metadata'))
return property(fget=fget)
-
-
+
+
def get_title(self):
title = self.metadata.find('dc:title')
if title and title.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
return self.default_title.strip()
-
+
def get_authors(self):
creators = self.metadata.findAll('dc:creator')
for elem in creators:
@@ -277,7 +276,7 @@ class OPF(MetaInformation):
ans.extend(i.split('&'))
return [a.strip() for a in ans]
return []
-
+
def get_author_sort(self):
creators = self.metadata.findAll('dc:creator')
for elem in creators:
@@ -288,37 +287,37 @@ class OPF(MetaInformation):
fa = elem.get('file-as')
return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None
return None
-
+
def get_title_sort(self):
title = self.package.find('dc:title')
if title:
if title.has_key('file-as'):
return title['file-as'].strip()
return None
-
+
def get_comments(self):
comments = self.soup.find('dc:description')
if comments and comments.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip()
return None
-
+
def get_uid(self):
package = self.package
if package.has_key('unique-identifier'):
return package['unique-identifier']
-
+
def get_category(self):
category = self.soup.find('dc:type')
if category and category.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
return None
-
+
def get_publisher(self):
publisher = self.soup.find('dc:publisher')
if publisher and publisher.string:
return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
return None
-
+
def get_isbn(self):
for item in self.metadata.findAll('dc:identifier'):
scheme = item.get('scheme')
@@ -327,13 +326,13 @@ class OPF(MetaInformation):
if scheme is not None and scheme.lower() == 'isbn' and item.string:
return str(item.string).strip()
return None
-
+
def get_language(self):
item = self.metadata.find('dc:language')
if not item:
return _('Unknown')
return ''.join(item.findAll(text=True)).strip()
-
+
def get_application_id(self):
for item in self.metadata.findAll('dc:identifier'):
scheme = item.get('scheme', None)
@@ -342,7 +341,7 @@ class OPF(MetaInformation):
if scheme in ['libprs500', 'calibre']:
return str(item.string).strip()
return None
-
+
def get_cover(self):
guide = getattr(self, '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]
if matches:
return matches[0].path
-
+
def possible_cover_prefixes(self):
isbn, ans = [], []
for item in self.metadata.findAll('dc:identifier'):
@@ -363,22 +362,22 @@ class OPF(MetaInformation):
for item in isbn:
ans.append(item[1].replace('-', ''))
return ans
-
+
def get_series(self):
s = self.metadata.find('series')
if s is not None:
return str(s.string).strip()
return None
-
+
def get_series_index(self):
s = self.metadata.find('series-index')
if s and s.string:
try:
- return int(str(s.string).strip())
+ return float(str(s.string).strip())
except:
return None
return None
-
+
def get_rating(self):
s = self.metadata.find('rating')
if s and s.string:
@@ -387,7 +386,7 @@ class OPF(MetaInformation):
except:
return None
return None
-
+
def get_tags(self):
ans = []
subs = self.soup.findAll('dc:subject')
@@ -396,17 +395,17 @@ class OPF(MetaInformation):
if val:
ans.append(val)
return [unicode(a).strip() for a in ans]
-
-
+
+
class OPFReader(OPF):
-
+
def __init__(self, stream, dir=os.getcwdu()):
manage = False
if not hasattr(stream, 'read'):
manage = True
dir = os.path.dirname(stream)
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'):
stream.seek(0)
self.soup = OPFSoup(stream.read())
@@ -420,18 +419,18 @@ class OPFReader(OPF):
spine = self.soup.find(re.compile('spine'))
if spine is not None:
self.spine = Spine.from_opf_spine_element(spine, self.manifest)
-
+
self.toc = TOC(base_path=dir)
self.toc.read_from_opf(self)
guide = self.soup.find(re.compile('guide'))
if guide is not None:
self.guide = Guide.from_opf_guide(guide, dir)
- self.base_dir = dir
+ self.base_dir = dir
self.cover_data = (None, None)
-
-
+
+
class OPFCreator(MetaInformation):
-
+
def __init__(self, base_path, *args, **kwargs):
'''
Initialize.
@@ -451,62 +450,62 @@ class OPFCreator(MetaInformation):
self.guide = Guide()
if self.cover:
self.guide.set_cover(self.cover)
-
-
+
+
def create_manifest(self, entries):
'''
Create
-
+
`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]),
entries)
self.manifest = Manifest.from_paths(entries)
self.manifest.set_basedir(self.base_path)
-
+
def create_manifest_from_files_in(self, files_and_dirs):
entries = []
-
+
def dodir(dir):
for spec in os.walk(dir):
root, files = spec[0], spec[-1]
for name in files:
path = os.path.join(root, name)
if os.path.isfile(path):
- entries.append((path, None))
-
+ entries.append((path, None))
+
for i in files_and_dirs:
if os.path.isdir(i):
dodir(i)
else:
entries.append((i, None))
-
- self.create_manifest(entries)
-
+
+ self.create_manifest(entries)
+
def create_spine(self, entries):
'''
Create the element. Must first call :method:`create_manifest`.
-
+
`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)
self.spine = Spine.from_paths(entries, self.manifest)
-
+
def set_toc(self, toc):
'''
Set the toc. You must call :method:`create_spine` before calling this
method.
-
+
:param toc: A :class:`TOC` object
'''
self.toc = toc
-
+
def create_guide(self, guide_element):
self.guide = Guide.from_opf_guide(guide_element, self.base_path)
self.guide.set_basedir(self.base_path)
-
+
def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
from calibre.resources import opf_template
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))
self.guide.set_cover(cover)
self.guide.set_basedir(self.base_path)
-
+
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
if not opf.startswith('\n'+opf
@@ -540,4 +539,4 @@ class OPFCreator(MetaInformation):
if toc is not None and ncx_stream is not None:
toc.render(ncx_stream, self.application_id)
ncx_stream.flush()
-
\ No newline at end of file
+
diff --git a/src/calibre/ebooks/metadata/opf.xml b/src/calibre/ebooks/metadata/opf.xml
index 027d560ffa..7acf0f5c78 100644
--- a/src/calibre/ebooks/metadata/opf.xml
+++ b/src/calibre/ebooks/metadata/opf.xml
@@ -9,15 +9,16 @@
${author}
${'%s (%s)'%(__appname__, __version__)} [http://${__appname__}.kovidgoyal.net]
${mi.application_id}
- ${mi.timestamp.isoformat()}
+ ${mi.pubdate.isoformat()}
${mi.language if mi.language else 'UND'}
${mi.category}
${mi.comments}
${mi.publisher}
${mi.isbn}
-
+
+
${tag}
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 7dc4c67d17..4d1af0ee75 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -442,9 +442,10 @@ class OPF(object):
comments = MetadataField('description')
category = MetadataField('category')
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)
- 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):
diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py
index c0a656d64d..952870fcd1 100644
--- a/src/calibre/ebooks/oeb/transforms/jacket.py
+++ b/src/calibre/ebooks/oeb/transforms/jacket.py
@@ -65,7 +65,7 @@ class Jacket(object):
comments = comments.replace('\r\n', '\n').replace('\n\n', '
')
series = 'Series: ' + mi.series if mi.series else ''
if series and mi.series_index is not None:
- series += ' [%s]'%mi.series_index
+ series += ' [%s]'%mi.format_series_index()
tags = mi.tags
if not tags:
try:
diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py
index cd6edcf699..894cb4fb08 100644
--- a/src/calibre/ebooks/oeb/transforms/metadata.py
+++ b/src/calibre/ebooks/oeb/transforms/metadata.py
@@ -58,7 +58,7 @@ class MergeMetadata(object):
m.add('creator', mi.book_producer, role='bkp')
if mi.series_index is not None:
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:
m.clear('rating')
m.add('rating', '%.2f'%mi.rating)
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 3fbc3a9e10..a8b6f2d05b 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -19,7 +19,8 @@ from calibre.ebooks.metadata import MetaInformation
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():
c = Config('gui', 'preferences for the calibre GUI')
diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py
index 2050108bde..70223acb70 100644
--- a/src/calibre/gui2/convert/__init__.py
+++ b/src/calibre/gui2/convert/__init__.py
@@ -119,7 +119,8 @@ class Widget(QWidget):
elif isinstance(g, XPathEdit):
g.edit.setText(val if val 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)
def set_help(self, msg):
diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py
index 31d8db0867..d961fef473 100644
--- a/src/calibre/gui2/convert/metadata.py
+++ b/src/calibre/gui2/convert/metadata.py
@@ -83,7 +83,7 @@ class MetadataWidget(Widget, Ui_Form):
comments = unicode(self.comment.toPlainText()).strip()
if 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:
mi.series = unicode(self.series.currentText()).strip()
tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')]
diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui
index 5b68d6383d..3721483893 100644
--- a/src/calibre/gui2/convert/metadata.ui
+++ b/src/calibre/gui2/convert/metadata.ui
@@ -1,7 +1,8 @@
-
+
+
Form
-
-
+
+
0
0
@@ -9,59 +10,89 @@
500
-
+
Form
-
+
-
-
-
+
+
Book Cover
-
-
-
-
-
+
+
-
+
+
-
+
+
+
+
+
+ :/images/book.svg
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
+ Use cover from &source file
+
+
+ true
+
+
+
+ -
+
+
6
-
+
0
-
-
-
+
+
Change &cover image:
-
+
cover_path
-
-
-
+
+
6
-
+
0
-
-
-
+
+
true
-
-
-
+
+
Browse for an image to use as the cover of this book.
-
+
...
-
-
+
+
:/images/document_open.svg:/images/document_open.svg
@@ -70,243 +101,204 @@
- -
-
-
- Use cover from &source file
-
-
- true
-
-
-
- -
-
-
-
-
-
-
-
-
- :/images/book.svg
-
-
- true
-
-
- Qt::AlignCenter
-
-
-
-
-
opt_prefer_metadata_cover
-
-
+
-
-
-
-
-
-
+
+
-
+
+
&Title:
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
title
- -
-
-
+
-
+
+
Change the title of this book
- -
-
-
+
-
+
+
&Author(s):
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
author
- -
-
-
-
+
-
+
+
+
1
0
-
+
Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it.
- -
-
-
+
-
+
+
Author So&rt:
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
author_sort
- -
-
-
-
+
-
+
+
+
0
0
-
+
Change the author(s) of this book. Multiple authors should be separated by a comma
- -
-
-
+
-
+
+
&Publisher:
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
publisher
- -
-
-
+
-
+
+
Change the publisher of this book
- -
-
-
+
-
+
+
Ta&gs:
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
tags
- -
-
-
- Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
+
-
+
+
+ Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
- -
-
-
+
-
+
+
&Series:
-
+
Qt::PlainText
-
+
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
+
series
- -
-
-
-
+
-
+
+
+
10
0
-
+
List of known series. You can add new series.
-
+
List of known series. You can add new series.
-
+
true
-
+
QComboBox::InsertAlphabetically
-
+
QComboBox::AdjustToContents
- -
-
-
- true
-
-
- Series index.
-
-
- Series index.
-
-
+
-
+
+
Book
-
- 1
+
+ 9999.989999999999782
-
- 10000
+
+ 1.000000000000000
-
-
-
-
+
+
+
0
0
-
+
16777215
200
-
+
Comments
-
-
-
-
-
+
+
-
+
+
16777215
180
@@ -329,8 +321,8 @@
-
-
+
+
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index 2c4ab859a3..4163c51583 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -177,7 +177,7 @@
- -
+
-
Rating of this book. 0-5 stars
@@ -309,28 +309,6 @@
- -
-
-
- false
-
-
- Series index.
-
-
- Series index.
-
-
- Book
-
-
- 0
-
-
- 10000
-
-
-
-
@@ -357,6 +335,19 @@
-
+ -
+
+
+ false
+
+
+ Book
+
+
+ 9999.989999999999782
+
+
+
@@ -640,7 +631,6 @@
series
tag_editor_button
remove_series_button
- series_index
isbn
comments
fetch_metadata_button
diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py
index c85c4248c8..21583e8f98 100644
--- a/src/calibre/gui2/library.py
+++ b/src/calibre/gui2/library.py
@@ -20,7 +20,7 @@ from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog
from calibre.utils.search_query_parser import SearchQueryParser
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):
COLOR = QColor("blue")
@@ -98,40 +98,38 @@ class DateDelegate(QStyledItemDelegate):
qde.setCalendarPopup(True)
return qde
-class BooksModel(QAbstractTableModel):
- 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"]
- )
+class PubDateDelegate(QStyledItemDelegate):
+ 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 = {
'title' : _("Title"),
'authors' : _("Author(s)"),
'size' : _("Size (MB)"),
'timestamp' : _("Date"),
+ 'pubdate' : _('Published'),
'rating' : _('Rating'),
'publisher' : _("Publisher"),
'tags' : _("Tags"),
'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):
QAbstractTableModel.__init__(self, parent)
self.db = None
self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
- 'tags', 'series', 'timestamp']
+ 'tags', 'series', 'timestamp', 'pubdate']
self.default_image = QImage(':/images/book.svg')
self.sorted_on = ('timestamp', Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model
@@ -157,8 +155,12 @@ class BooksModel(QAbstractTableModel):
tidx = self.column_map.index('timestamp')
except ValueError:
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):
@@ -186,8 +188,8 @@ class BooksModel(QAbstractTableModel):
self.db = None
self.reset()
- def add_books(self, paths, formats, metadata, uris=[], add_duplicates=False):
- ret = self.db.add_books(paths, formats, metadata, uris,
+ def add_books(self, paths, formats, metadata, add_duplicates=False):
+ ret = self.db.add_books(paths, formats, metadata,
add_duplicates=add_duplicates)
self.count_changed()
return ret
@@ -313,7 +315,7 @@ class BooksModel(QAbstractTableModel):
series = self.db.series(idx)
if series:
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 %s of %s.')%(sidx, series)
return data
@@ -492,6 +494,7 @@ class BooksModel(QAbstractTableModel):
ridx = FIELD_MAP['rating']
pidx = FIELD_MAP['publisher']
tmdx = FIELD_MAP['timestamp']
+ pddx = FIELD_MAP['pubdate']
srdx = FIELD_MAP['series']
tgdx = FIELD_MAP['tags']
siix = FIELD_MAP['series_index']
@@ -508,6 +511,12 @@ class BooksModel(QAbstractTableModel):
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
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):
r = self.db.data[r][ridx]
r = r/2 if r else 0
@@ -526,8 +535,8 @@ class BooksModel(QAbstractTableModel):
def series(r):
series = self.db.data[r][srdx]
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):
size = self.db.data[r][sidx]
if size:
@@ -538,6 +547,7 @@ class BooksModel(QAbstractTableModel):
'authors' : authors,
'size' : size,
'timestamp': timestamp,
+ 'pubdate' : pubdate,
'rating' : rating,
'publisher': publisher,
'tags' : tags,
@@ -577,7 +587,7 @@ class BooksModel(QAbstractTableModel):
if column not in self.editable_cols:
return False
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())
id = self.db.id(row)
if column == 'rating':
@@ -585,10 +595,10 @@ class BooksModel(QAbstractTableModel):
val *= 2
self.db.set_rating(id, val)
elif column == 'series':
- pat = re.compile(r'\[(\d+)\]')
+ pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(val)
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 = val.strip()
if val:
@@ -598,6 +608,11 @@ class BooksModel(QAbstractTableModel):
return False
dt = datetime(val.year(), val.month(), val.day()) + timedelta(seconds=time.timezone) - timedelta(hours=time.daylight)
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:
self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
@@ -625,29 +640,35 @@ class BooksView(TableView):
TableView.__init__(self, parent)
self.rating_delegate = LibraryDelegate(self)
self.timestamp_delegate = DateDelegate(self)
+ self.pubdate_delegate = PubDateDelegate(self)
self.display_parent = parent
self._model = modelcls(self)
self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
try:
- self.columns_sorted(self._model.column_map.index('rating'),
- self._model.column_map.index('timestamp'))
+ cm = self._model.column_map
+ 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:
pass
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
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)):
if self.itemDelegateForColumn(i) in (self.rating_delegate,
- self.timestamp_delegate):
+ self.timestamp_delegate, self.pubdate_delegate):
self.setItemDelegateForColumn(i, self.itemDelegate())
if rating_col > -1:
self.setItemDelegateForColumn(rating_col, self.rating_delegate)
if timestamp_col > -1:
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,
save, open_folder, book_details, similar_menu=None):
diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py
index cf352c464d..7261aed7ad 100644
--- a/src/calibre/library/database.py
+++ b/src/calibre/library/database.py
@@ -27,7 +27,7 @@ class Concatenate(object):
return self.ans[:-len(self.sep)]
return self.ans
class Connection(sqlite.Connection):
-
+
def get(self, *args, **kw):
ans = self.execute(*args)
if not kw.get('all', True):
@@ -785,8 +785,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
FROM books;
''')
conn.execute('pragma user_version=12')
- conn.commit()
-
+ conn.commit()
+
def __init__(self, dbpath, row_factory=False):
self.dbpath = dbpath
self.conn = _connect(dbpath)
@@ -901,7 +901,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def id(self, index):
return self.data[index][0]
-
+
def row(self, id):
for r, record in enumerate(self.data):
if record[0] == id:
@@ -916,8 +916,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return _('Unknown')
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
'''
if not index_is_id:
@@ -939,11 +939,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
if index_is_id:
return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False)
return self.data[index][3]
-
+
def publisher_id(self, index, index_is_id=False):
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)
-
+
def rating(self, index, index_is_id=False):
if index_is_id:
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):
id = self.series_id(index, index_is_id)
return self.conn.get('SELECT name from series WHERE id=?', (id,), all=False)
-
+
def series_index(self, index, index_is_id=False):
ans = None
if not index_is_id:
@@ -991,9 +991,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
else:
ans = self.conn.get('SELECT series_index FROM books WHERE id=?', (index,), all=False)
try:
- return int(ans)
+ return float(ans)
except:
- return 1
+ return 1.0
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'''
id = index if index_is_id else self.id(index)
return self.conn.get('SELECT text FROM comments WHERE book=?', (id,), all=False)
-
+
def formats(self, index, index_is_id=False):
''' Return available formats as a comma separated list '''
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):
return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM series')]
-
+
def all_authors(self):
return [ (i[0], i[1]) for i in \
self.conn.get('SELECT id, name FROM authors')]
-
+
def all_publishers(self):
return [ (i[0], i[1]) for i in \
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)
if mi.cover_data[1] is not None:
self.set_cover(id, mi.cover_data[1])
-
-
-
+
+
+
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):
return [i[0] for i in self.conn.get('SELECT id FROM books')]
-
-
+
+
def has_id(self, id):
return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None
-
-
+
+
class SearchToken(object):
@@ -1455,4 +1455,4 @@ def text_to_tokens(text):
if __name__ == '__main__':
sqlite.enable_callback_tracebacks(True)
- db = LibraryDatabase('/home/kovid/temp/library1.db.orig')
\ No newline at end of file
+ db = LibraryDatabase('/home/kovid/temp/library1.db.orig')
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 1b373bf738..9803721453 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -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,
'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()))
@@ -472,6 +473,53 @@ class LibraryDatabase2(LibraryDatabase):
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):
''' Return last modified time as a UTC datetime object'''
@@ -610,6 +658,16 @@ class LibraryDatabase2(LibraryDatabase):
return img
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):
'''
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.publisher = self.publisher(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)
if tags:
mi.tags = [i.strip() for i in tags.split(',')]
@@ -917,7 +976,7 @@ class LibraryDatabase2(LibraryDatabase):
self.set_comment(id, mi.comments, notify=False)
if mi.isbn and mi.isbn.strip():
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)
if getattr(mi, 'timestamp', None) is not None:
self.set_timestamp(id, mi.timestamp, notify=False)
@@ -983,6 +1042,15 @@ class LibraryDatabase2(LibraryDatabase):
if notify:
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):
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')
@@ -1103,17 +1171,11 @@ class LibraryDatabase2(LibraryDatabase):
def set_series_index(self, id, idx, notify=True):
if idx is None:
- idx = 1
- idx = int(idx)
- self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id))
+ idx = 1.0
+ idx = float(idx)
+ self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (idx, id))
self.conn.commit()
- try:
- 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)
+ self.data.set(id, FIELD_MAP['series_index'], idx, row_is_id=True)
if notify:
self.notify('metadata', [id])
@@ -1156,7 +1218,7 @@ class LibraryDatabase2(LibraryDatabase):
stream.seek(0)
mi = get_metadata(stream, format, use_libprs_metadata=False)
stream.seek(0)
- mi.series_index = 1
+ mi.series_index = 1.0
mi.tags = [_('News'), recipe.title]
obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)',
(mi.title, mi.authors[0]))
@@ -1188,7 +1250,7 @@ class LibraryDatabase2(LibraryDatabase):
def create_book_entry(self, mi, cover=None, add_duplicates=True):
if not add_duplicates and self.has_book(mi):
return None
- series_index = 1 if mi.series_index is None else mi.series_index
+ 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)
title = mi.title
if isinstance(aus, str):
@@ -1207,33 +1269,29 @@ class LibraryDatabase2(LibraryDatabase):
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.
: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 = []
ids = []
for path in paths:
mi = metadata.next()
format = formats.next()
- try:
- uri = uris.next()
- except StopIteration:
- uri = None
if not add_duplicates and self.has_book(mi):
- duplicates.append((path, format, mi, uri))
+ duplicates.append((path, format, mi))
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)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
if isinstance(title, str):
title = title.decode(preferred_encoding)
- obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
- (title, uri, series_index, aus))
+ obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
+ (title, series_index, aus))
id = obj.lastrowid
self.data.books_added([id], self.conn)
ids.append(id)
@@ -1251,12 +1309,11 @@ class LibraryDatabase2(LibraryDatabase):
paths = list(duplicate[0] for duplicate in duplicates)
formats = list(duplicate[1] for duplicate in duplicates)
metadata = list(duplicate[2] for duplicate in duplicates)
- uris = list(duplicate[3] for duplicate in duplicates)
- return (paths, formats, metadata, uris), len(ids)
+ return (paths, formats, metadata), len(ids)
return None, len(ids)
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:
mi.title = _('Unknown')
if not mi.authors:
@@ -1266,8 +1323,8 @@ class LibraryDatabase2(LibraryDatabase):
aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \
mi.title.decode(preferred_encoding, 'replace')
- obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)',
- (title, None, series_index, aus))
+ obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
+ (title, series_index, aus))
id = obj.lastrowid
self.data.books_added([id], self.conn)
self.set_path(id, True)
@@ -1368,12 +1425,12 @@ class LibraryDatabase2(LibraryDatabase):
QCoreApplication.processEvents()
db.conn.row_factory = lambda cursor, row : tuple(row)
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.setRange(0, len(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 = '''
authors ratings tags series books_tags_link
diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py
index 26788bcf6d..3a4b0131cf 100644
--- a/src/calibre/library/server.py
+++ b/src/calibre/library/server.py
@@ -25,6 +25,7 @@ from calibre.library.database2 import LibraryDatabase2, FIELD_MAP
from calibre.utils.config import config_dir
from calibre.utils.mdns import publish as publish_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')
server_resources['jquery.js'] = jquery
@@ -271,7 +272,7 @@ class LibraryServer(object):
@expose
def stanza(self):
- ' Feeds to read calibre books on a ipod with stanza.'
+ 'Feeds to read calibre books on a ipod with stanza.'
books = []
for record in iter(self.db):
r = record[FIELD_MAP['formats']]
@@ -289,8 +290,8 @@ class LibraryServer(object):
extra.append('TAGS: %s
'%', '.join(tags.split(',')))
series = record[FIELD_MAP['series']]
if series:
- extra.append('SERIES: %s [%d]
'%(series,
- record[FIELD_MAP['series_index']]))
+ extra.append('SERIES: %s [%s]
'%(series,
+ fmt_sidx(record[FIELD_MAP['series_index']])))
fmt = 'epub' if 'EPUB' in r else 'pdb'
mimetype = guess_type('dummy.'+fmt)[0]
books.append(self.STANZA_ENTRY.generate(
@@ -339,6 +340,7 @@ class LibraryServer(object):
for record in items[start:start+num]:
aus = record[2] if record[2] else __builtins__._('Unknown')
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'))
updated = self.db.last_modified()
diff --git a/todo b/todo
index bff97ad7e8..9b2a90f0b2 100644
--- a/todo
+++ b/todo
@@ -2,6 +2,7 @@
* 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.
+ - test adding/recusrsize adding and adding of duplicates
* Testing framework