Stanza integration: Use a UUID instead of the database rowid as a unique identifier for each book. This means that, only after this upgrade, Stanza will forget which books it has already downloaded. Fixes #3137 (Provide optional user-customizable "Stanza Unique Identifier"). Also use a UUID when converting to EPUB as the book identifier.

This commit is contained in:
Kovid Goyal 2009-10-28 23:21:36 -06:00
parent b5fcc2466b
commit 4c415a5ce0
10 changed files with 106 additions and 22 deletions

View File

@ -12,7 +12,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class DailyTelegraph(BasicNewsRecipe): class DailyTelegraph(BasicNewsRecipe):
title = u'Daily Telegraph' title = u'Daily Telegraph'
__author__ = u'AprilHare' __author__ = u'AprilHare'
language = 'en' language = 'en_AU'
description = u'News from down under' description = u'News from down under'
oldest_article = 2 oldest_article = 2

View File

@ -320,8 +320,8 @@ class HTMLInput(InputFormatPlugin):
oeb.logger.warn('Title not specified') oeb.logger.warn('Title not specified')
metadata.add('title', self.oeb.translate(__('Unknown'))) metadata.add('title', self.oeb.translate(__('Unknown')))
bookid = "urn:uuid:%s" % str(uuid.uuid4()) bookid = str(uuid.uuid4())
metadata.add('identifier', bookid, id='calibre-uuid') metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
for ident in metadata.identifier: for ident in metadata.identifier:
if 'id' in ident.attrib: if 'id' in ident.attrib:
self.oeb.uid = metadata.identifier[0] self.oeb.uid = metadata.identifier[0]

View File

@ -218,7 +218,7 @@ class MetaInformation(object):
'isbn', 'tags', 'cover_data', 'application_id', 'guide', 'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language', 'manifest', 'spine', 'toc', 'cover', 'language',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
'pubdate', 'rights', 'publication_type'): 'pubdate', 'rights', 'publication_type', 'uuid'):
if hasattr(mi, attr): if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr)) setattr(ans, attr, getattr(mi, attr))
@ -244,7 +244,7 @@ class MetaInformation(object):
'series', 'series_index', 'rating', 'isbn', 'language', 'series', 'series_index', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
'rights', 'publication_type', 'rights', 'publication_type', 'uuid',
): ):
setattr(self, x, getattr(mi, x, None)) setattr(self, x, getattr(mi, x, None))
@ -264,7 +264,7 @@ class MetaInformation(object):
'isbn', 'application_id', 'manifest', 'spine', 'toc', 'isbn', 'application_id', 'manifest', 'spine', 'toc',
'cover', 'language', 'guide', 'book_producer', 'cover', 'language', 'guide', 'book_producer',
'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', 'rights',
'publication_type'): 'publication_type', 'uuid',):
if hasattr(mi, attr): if hasattr(mi, attr):
val = getattr(mi, attr) val = getattr(mi, attr)
if val is not None: if val is not None:

View File

@ -432,6 +432,9 @@ class OPF(object):
identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]') identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]')
application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]') '(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]')
uuid_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+
'(re:match(@opf:scheme, "uuid", "i") or re:match(@scheme, "uuid", "i"))]')
manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]') manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]')
manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]')
spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]') spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]')
@ -747,6 +750,25 @@ class OPF(object):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@dynamic_property
def uuid(self):
def fget(self):
for match in self.uuid_id_path(self.metadata):
return self.get_text(match) or None
def fset(self, val):
matches = self.uuid_id_path(self.metadata)
if not matches:
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'uuid'}
matches = [self.create_metadata_element('identifier',
attrib=attrib)]
self.set_text(matches[0], unicode(val))
return property(fget=fget, fset=fset)
@dynamic_property @dynamic_property
def book_producer(self): def book_producer(self):
@ -977,6 +999,9 @@ def metadata_to_opf(mi, as_string=True):
if not mi.application_id: if not mi.application_id:
mi.application_id = str(uuid.uuid4()) mi.application_id = str(uuid.uuid4())
if not mi.uuid:
mi.uuid = str(uuid.uuid4())
if not mi.book_producer: if not mi.book_producer:
mi.book_producer = __appname__ + ' (%s) '%__version__ + \ mi.book_producer = __appname__ + ' (%s) '%__version__ + \
'[http://calibre-ebook.com]' '[http://calibre-ebook.com]'
@ -986,13 +1011,14 @@ def metadata_to_opf(mi, as_string=True):
root = etree.fromstring(textwrap.dedent( root = etree.fromstring(textwrap.dedent(
''' '''
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="%(a)s_id"> <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier> <dc:identifier opf:scheme="%(a)s" id="%(a)s_id">%(id)s</dc:identifier>
<dc:identifier opf:scheme="uuid" id="uuid_id">%(uuid)s</dc:identifier>
</metadata> </metadata>
<guide/> <guide/>
</package> </package>
'''%dict(a=__appname__, id=mi.application_id))) '''%dict(a=__appname__, id=mi.application_id, uuid=mi.uuid)))
metadata = root[0] metadata = root[0]
guide = root[1] guide = root[1]
metadata[0].tail = '\n'+(' '*8) metadata[0].tail = '\n'+(' '*8)

View File

@ -139,10 +139,9 @@ class OEBReader(object):
mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\ mi.book_producer = '%(a)s (%(v)s) [http://%(a)s.kovidgoyal.net]'%\
dict(a=__appname__, v=__version__) dict(a=__appname__, v=__version__)
meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger) meta_info_to_oeb_metadata(mi, self.oeb.metadata, self.logger)
bookid = "urn:uuid:%s" % str(uuid.uuid4()) if mi.application_id is None \ self.oeb.metadata.add('identifier', str(uuid.uuid4()), id='uuid_id',
else mi.application_id scheme='uuid')
self.oeb.metadata.add('identifier', bookid, id='calibre-uuid') self.oeb.uid = self.oeb.metadata.identifier[-1]
self.oeb.uid = self.oeb.metadata.identifier[0]
def _manifest_prune_invalid(self): def _manifest_prune_invalid(self):
''' '''

View File

@ -80,12 +80,19 @@ class MergeMetadata(object):
def __call__(self, oeb, mi, opts): def __call__(self, oeb, mi, opts):
self.oeb, self.log = oeb, oeb.log self.oeb, self.log = oeb, oeb.log
m = self.oeb.metadata m = self.oeb.metadata
meta_info_to_oeb_metadata(mi, m, oeb.log)
self.log('Merging user specified metadata...') self.log('Merging user specified metadata...')
meta_info_to_oeb_metadata(mi, m, oeb.log)
cover_id = self.set_cover(mi, opts.prefer_metadata_cover) cover_id = self.set_cover(mi, opts.prefer_metadata_cover)
m.clear('cover') m.clear('cover')
if cover_id is not None: if cover_id is not None:
m.add('cover', cover_id) m.add('cover', cover_id)
if mi.uuid is not None:
m.filter('identifier', lambda x:x.id=='uuid_id')
self.oeb.metadata.add('identifier', mi.uuid, id='uuid_id',
scheme='uuid')
self.oeb.uid = self.oeb.metadata.identifier[-1]
def set_cover(self, mi, prefer_metadata_cover): def set_cover(self, mi, prefer_metadata_cover):

View File

@ -18,7 +18,9 @@ from calibre.library.database2 import LibraryDatabase2
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
from calibre.utils.genshi.template import MarkupTemplate from calibre.utils.genshi.template import MarkupTemplate
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'cover']) FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating',
'timestamp', 'size', 'tags', 'comments', 'series', 'series_index',
'formats', 'isbn', 'uuid', 'cover'])
XML_TEMPLATE = '''\ XML_TEMPLATE = '''\
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
@ -26,6 +28,7 @@ XML_TEMPLATE = '''\
<py:for each="record in data"> <py:for each="record in data">
<record> <record>
<id>${record['id']}</id> <id>${record['id']}</id>
<uuid>${record['uuid']}</uuid>
<title>${record['title']}</title> <title>${record['title']}</title>
<authors sort="${record['author_sort']}"> <authors sort="${record['author_sort']}">
<py:for each="author in record['authors']"> <py:for each="author in record['authors']">
@ -71,7 +74,7 @@ STANZA_TEMPLATE='''\
<py:for each="record in data"> <py:for each="record in data">
<entry> <entry>
<title>${record['title']}</title> <title>${record['title']}</title>
<id>urn:calibre:${record['id']}</id> <id>urn:calibre:${record['uuid']}</id>
<author><name>${record['author_sort']}</name></author> <author><name>${record['author_sort']}</name></author>
<updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated> <updated>${record['timestamp'].strftime('%Y-%m-%dT%H:%M:%SZ')}</updated>
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" /> <link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/')).replace('http%3A', 'http:')}" />
@ -227,7 +230,7 @@ def command_list(args, dbpath):
if not set(fields).issubset(FIELDS): if not set(fields).issubset(FIELDS):
parser.print_help() parser.print_help()
print print
print >>sys.stderr, _('Invalid fields. Available fields:'), ','.join(FIELDS) print >>sys.stderr, _('Invalid fields. Available fields:'), ','.join(sorted(FIELDS))
return 1 return 1
db = get_db(dbpath, opts) db = get_db(dbpath, opts)

View File

@ -59,7 +59,7 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5, FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10, 'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15, 'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
'lccn':16, 'pubdate':17, 'flags':18, 'cover':19} 'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19, 'cover':20}
INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys())) INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys()))
@ -447,7 +447,7 @@ class LibraryDatabase2(LibraryDatabase):
for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn', for prop in ('author_sort', 'authors', 'comment', 'comments', 'isbn',
'publisher', 'rating', 'series', 'series_index', 'tags', 'publisher', 'rating', 'series', 'series_index', 'tags',
'title', 'timestamp'): 'title', 'timestamp', 'uuid'):
setattr(self, prop, functools.partial(get_property, setattr(self, prop, functools.partial(get_property,
loc=FIELD_MAP['comments' if prop == 'comment' else prop])) loc=FIELD_MAP['comments' if prop == 'comment' else prop]))
@ -622,6 +622,50 @@ class LibraryDatabase2(LibraryDatabase):
END TRANSACTION; END TRANSACTION;
''') ''')
def upgrade_version_7(self):
'Add uuid column'
self.conn.executescript('''
BEGIN TRANSACTION;
ALTER TABLE books ADD COLUMN uuid TEXT;
DROP TRIGGER IF EXISTS books_insert_trg;
DROP TRIGGER IF EXISTS books_update_trg;
UPDATE books SET uuid=uuid4();
CREATE TRIGGER books_insert_trg AFTER INSERT ON books
BEGIN
UPDATE books SET sort=title_sort(NEW.title),uuid=uuid4() WHERE id=NEW.id;
END;
CREATE TRIGGER books_update_trg AFTER UPDATE ON books
BEGIN
UPDATE books SET sort=title_sort(NEW.title) WHERE id=NEW.id;
END;
DROP VIEW meta;
CREATE VIEW meta AS
SELECT id, title,
(SELECT sortconcat(bal.id, name) FROM books_authors_link AS bal JOIN authors ON(author = authors.id) 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,
uuid
FROM books;
END TRANSACTION;
''')
def last_modified(self): def last_modified(self):
@ -785,6 +829,7 @@ class LibraryDatabase2(LibraryDatabase):
mi.publisher = self.publisher(idx, index_is_id=index_is_id) mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id) mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id) mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
mi.uuid = self.uuid(idx, index_is_id=index_is_id)
tags = self.tags(idx, index_is_id=index_is_id) tags = self.tags(idx, index_is_id=index_is_id)
if tags: if tags:
mi.tags = [i.strip() for i in tags.split(',')] mi.tags = [i.strip() for i in tags.split(',')]
@ -1530,7 +1575,9 @@ class LibraryDatabase2(LibraryDatabase):
''' '''
if prefix is None: if prefix is None:
prefix = self.library_path prefix = self.library_path
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size', 'tags', 'comments', 'series', 'series_index', 'isbn']) FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating',
'timestamp', 'size', 'tags', 'comments', 'series', 'series_index',
'isbn', 'uuid'])
data = [] data = []
for record in self.data: for record in self.data:
if record is None: continue if record is None: continue

View File

@ -242,7 +242,7 @@ class LibraryServer(object):
STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\ STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
<entry xmlns:py="http://genshi.edgewall.org/"> <entry xmlns:py="http://genshi.edgewall.org/">
<title>${record[FM['title']]}</title> <title>${record[FM['title']]}</title>
<id>urn:calibre:${record[FM['id']]}</id> <id>urn:calibre:${urn}</id>
<author><name>${authors}</name></author> <author><name>${authors}</name></author>
<updated>${timestamp}</updated> <updated>${timestamp}</updated>
<link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" /> <link type="${mimetype}" href="/get/${fmt}/${record[FM['id']]}" />
@ -678,6 +678,7 @@ class LibraryServer(object):
extra='\n'.join(extra), extra='\n'.join(extra),
mimetype=mimetype, mimetype=mimetype,
fmt=fmt, fmt=fmt,
urn=record[FIELD_MAP['uuid']],
timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5]) timestamp=strftime('%Y-%m-%dT%H:%M:%S+00:00', record[5])
) )
books.append(self.STANZA_ENTRY.generate(**data)\ books.append(self.STANZA_ENTRY.generate(**data)\

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
Wrapper for multi-threaded access to a single sqlite database connection. Serializes Wrapper for multi-threaded access to a single sqlite database connection. Serializes
all calls. all calls.
''' '''
import sqlite3 as sqlite, traceback, time import sqlite3 as sqlite, traceback, time, uuid
from sqlite3 import IntegrityError from sqlite3 import IntegrityError
from threading import Thread from threading import Thread
from Queue import Queue from Queue import Queue
@ -121,6 +121,7 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_function('title_sort', 1, title_sort) self.conn.create_function('title_sort', 1, title_sort)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
def run(self): def run(self):
try: try: