From 3585977bb399ea4aee18aac057ca18f5add477e2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 30 May 2009 23:37:05 -0700 Subject: [PATCH] Make series index a float and add a column for publish date --- src/calibre/ebooks/metadata/__init__.py | 40 ++- src/calibre/ebooks/metadata/fb2.py | 6 +- src/calibre/ebooks/metadata/meta.py | 2 +- src/calibre/ebooks/metadata/opf.py | 189 ++++++------ src/calibre/ebooks/metadata/opf.xml | 5 +- src/calibre/ebooks/metadata/opf2.py | 5 +- src/calibre/ebooks/oeb/transforms/jacket.py | 2 +- src/calibre/ebooks/oeb/transforms/metadata.py | 2 +- src/calibre/gui2/__init__.py | 3 +- src/calibre/gui2/convert/__init__.py | 3 +- src/calibre/gui2/convert/metadata.py | 2 +- src/calibre/gui2/convert/metadata.ui | 280 +++++++++--------- src/calibre/gui2/dialogs/metadata_single.ui | 38 +-- src/calibre/gui2/library.py | 85 ++++-- src/calibre/library/database.py | 44 +-- src/calibre/library/database2.py | 119 ++++++-- src/calibre/library/server.py | 8 +- todo | 1 + 18 files changed, 465 insertions(+), 369 deletions(-) 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'%s
'%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