Set cover in MOBI files when saving to disk. Also update metadata in LRF, EPUB and MOBI files when sending to device.

This commit is contained in:
Kovid Goyal 2009-02-08 17:10:53 -08:00
parent 2a474382d0
commit f8aa3bf8de
7 changed files with 173 additions and 139 deletions

View File

@ -18,7 +18,7 @@ from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre.ebooks.metadata import get_parser, MetaInformation from calibre.ebooks.metadata import get_parser, MetaInformation
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre import CurrentDir, fit_image from calibre import CurrentDir
class EPubException(Exception): class EPubException(Exception):
pass pass

View File

@ -23,8 +23,8 @@ class StreamSlicer(object):
def __init__(self, stream, start=0, stop=None): def __init__(self, stream, start=0, stop=None):
self._stream = stream self._stream = stream
self.start = start self.start = start
if stop is None: if stop is None:
stream.seek(0, 2) stream.seek(0, 2)
stop = stream.tell() stop = stream.tell()
self.stop = stop self.stop = stop
self._len = stop - start self._len = stop - start
@ -74,7 +74,7 @@ class StreamSlicer(object):
raise TypeError("stream indices must be integers") raise TypeError("stream indices must be integers")
class MetadataUpdater(object): class MetadataUpdater(object):
def __init__(self, stream): def __init__(self, stream):
self.stream = stream self.stream = stream
data = self.data = StreamSlicer(stream) data = self.data = StreamSlicer(stream)
@ -151,18 +151,22 @@ class MetadataUpdater(object):
self.exth[:] = ''.join([exth, title, '\0' * trail]) self.exth[:] = ''.join([exth, title, '\0' * trail])
self.record0[84:92] = pack('>II', title_off, title_len) self.record0[84:92] = pack('>II', title_off, title_len)
self.record0[92:96] = iana2mobi(mi.language) self.record0[92:96] = iana2mobi(mi.language)
if mi.cover_data[1]: if mi.cover_data[1] or mi.cover:
data = mi.cover_data[1] try:
if self.cover_record is not None: data = mi.cover_data[1] if mi.cover_data[1] else open(mi.cover, 'rb').read()
size = len(self.cover_record) except:
cover = rescale_image(data, size) pass
cover += '\0' * (size - len(cover)) else:
self.cover_record[:] = cover if self.cover_record is not None:
if self.thumbnail_record is not None: size = len(self.cover_record)
size = len(self.thumbnail_record) cover = rescale_image(data, size)
thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN) cover += '\0' * (size - len(cover))
thumbnail += '\0' * (size - len(thumbnail)) self.cover_record[:] = cover
self.thumbnail_record[:] = thumbnail if self.thumbnail_record is not None:
size = len(self.thumbnail_record)
thumbnail = rescale_image(data, size, dimen=MAX_THUMB_DIMEN)
thumbnail += '\0' * (size - len(thumbnail))
self.thumbnail_record[:] = thumbnail
return return
def set_metadata(stream, mi): def set_metadata(stream, mi):

View File

@ -306,13 +306,15 @@ IANA_MOBI = \
'zu': {None: (53, 0)}} 'zu': {None: (53, 0)}}
def iana2mobi(icode): def iana2mobi(icode):
subtags = list(icode.split('-')) langdict, subtags = IANA_MOBI[None], []
langdict = IANA_MOBI[None] if icode:
while len(subtags) > 0: subtags = list(icode.split('-'))
lang = subtags.pop(0).lower() while len(subtags) > 0:
if lang in IANA_MOBI: lang = subtags.pop(0).lower()
langdict = IANA_MOBI[lang] if lang in IANA_MOBI:
break langdict = IANA_MOBI[lang]
break
mcode = langdict[None] mcode = langdict[None]
while len(subtags) > 0: while len(subtags) > 0:
subtag = subtags.pop(0) subtag = subtags.pop(0)

View File

@ -18,6 +18,7 @@ from calibre.library.database2 import FIELD_MAP
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \ from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog error_dialog
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
class LibraryDelegate(QItemDelegate): class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
@ -423,7 +424,7 @@ class BooksModel(QAbstractTableModel):
def get_preferred_formats(self, rows, formats, paths=False): def get_preferred_formats(self, rows, formats, paths=False, set_metadata=False):
ans = [] ans = []
for row in (row.row() for row in rows): for row in (row.row() for row in rows):
format = None format = None
@ -441,6 +442,9 @@ class BooksModel(QAbstractTableModel):
pt = PersistentTemporaryFile(suffix='.'+format) pt = PersistentTemporaryFile(suffix='.'+format)
pt.write(self.db.format(row, format)) pt.write(self.db.format(row, format))
pt.flush() pt.flush()
if set_metadata:
_set_metadata(pt, self.db.get_metadata(row, get_cover=True),
format)
pt.close() if paths else pt.seek(0) pt.close() if paths else pt.seek(0)
ans.append(pt) ans.append(pt)
else: else:

View File

@ -932,7 +932,8 @@ class Main(MainWindow, Ui_MainWindow):
mi['cover'] = self.cover_to_thumbnail(cdata) mi['cover'] = self.cover_to_thumbnail(cdata)
metadata = iter(metadata) metadata = iter(metadata)
_files = self.library_view.model().get_preferred_formats(rows, _files = self.library_view.model().get_preferred_formats(rows,
self.device_manager.device_class.FORMATS, paths=True) self.device_manager.device_class.FORMATS,
paths=True, set_metadata=True)
files = [getattr(f, 'name', None) for f in _files] files = [getattr(f, 'name', None) for f in _files]
bad, good, gf, names, remove_ids = [], [], [], [], [] bad, good, gf, names, remove_ids = [], [], [], [], []
for f in files: for f in files:

View File

@ -4,12 +4,11 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
Backend that implements storage of ebooks in an sqlite database. Backend that implements storage of ebooks in an sqlite database.
''' '''
import sqlite3 as sqlite import sqlite3 as sqlite
import datetime, re, os, cPickle, traceback, sre_constants import datetime, re, os, cPickle, sre_constants
from zlib import compress, decompress from zlib import compress, decompress
from calibre import sanitize_file_name from calibre import sanitize_file_name
from calibre.ebooks.metadata.meta import set_metadata, metadata_from_formats from calibre.ebooks.metadata.meta import set_metadata, metadata_from_formats
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.web.feeds.recipes import migrate_automatic_profile_to_automatic_recipe from calibre.web.feeds.recipes import migrate_automatic_profile_to_automatic_recipe
@ -1389,77 +1388,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
def all_ids(self): def all_ids(self):
return [i[0] for i in self.conn.get('SELECT id FROM books')] return [i[0] for i in self.conn.get('SELECT id FROM books')]
def export_to_dir(self, dir, indices, byauthor=False, single_dir=False,
index_is_id=False, callback=None):
if not os.path.exists(dir):
raise IOError('Target directory does not exist: '+dir)
by_author = {}
count = 0
for index in indices:
id = index if index_is_id else self.id(index)
au = self.conn.get('SELECT author_sort FROM books WHERE id=?',
(id,), all=False)
if not au:
au = self.authors(index, index_is_id=index_is_id)
if not au:
au = _('Unknown')
au = au.split(',')[0]
if not by_author.has_key(au):
by_author[au] = []
by_author[au].append(index)
for au in by_author.keys():
apath = os.path.join(dir, sanitize_file_name(au))
if not single_dir and not os.path.exists(apath):
os.mkdir(apath)
for idx in by_author[au]:
title = re.sub(r'\s', ' ', self.title(idx, index_is_id=index_is_id))
tpath = os.path.join(apath, sanitize_file_name(title))
id = idx if index_is_id else self.id(idx)
id = str(id)
if not single_dir and not os.path.exists(tpath):
os.mkdir(tpath)
name = au + ' - ' + title if byauthor else title + ' - ' + au
name += '_'+id
base = dir if single_dir else tpath
mi = self.get_metadata(idx, index_is_id=index_is_id)
cover = self.cover(idx, index_is_id=index_is_id)
if cover is not None:
cname = sanitize_file_name(name) + '.jpg'
cpath = os.path.join(base, cname)
open(cpath, 'wb').write(cover)
mi.cover = cname
f = open(os.path.join(base, sanitize_file_name(name)+'.opf'), 'wb')
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(base, mi)
opf.render(f)
f.close()
fmts = self.formats(idx, index_is_id=index_is_id)
if not fmts:
fmts = ''
for fmt in fmts.split(','):
data = self.format(idx, fmt, index_is_id=index_is_id)
if not data:
continue
fname = name +'.'+fmt.lower()
fname = sanitize_file_name(fname)
f = open(os.path.join(base, fname), 'w+b')
f.write(data)
f.flush()
f.seek(0)
try:
set_metadata(f, mi, fmt.lower())
except:
print 'Error setting metadata for book:', mi.title
traceback.print_exc()
f.close()
count += 1
if callable(callback):
if not callback(count, mi.title):
return
def import_book(self, mi, formats): def import_book(self, mi, formats):
@ -1573,43 +1502,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
return duplicates return duplicates
def export_single_format_to_dir(self, dir, indices, format,
index_is_id=False, callback=None):
dir = os.path.abspath(dir)
if not index_is_id:
indices = map(self.id, indices)
failures = []
for count, id in enumerate(indices):
try:
data = self.format(id, format, index_is_id=True)
if not data:
failures.append((id, self.title(id, index_is_id=True)))
continue
except:
failures.append((id, self.title(id, index_is_id=True)))
continue
title = self.title(id, index_is_id=True)
au = self.authors(id, index_is_id=True)
if not au:
au = _('Unknown')
fname = '%s - %s.%s'%(title, au, format.lower())
fname = sanitize_file_name(fname)
if not os.path.exists(dir):
os.makedirs(dir)
f = open(os.path.join(dir, fname), 'w+b')
f.write(data)
f.seek(0)
try:
set_metadata(f, self.get_metadata(id, index_is_id=True), stream_type=format.lower())
except:
pass
f.close()
if callable(callback):
if not callback(count, title):
break
return failures
class SearchToken(object): class SearchToken(object):

View File

@ -19,8 +19,9 @@ from calibre.library import title_sort
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.library.sqlite import connect, IntegrityError from calibre.library.sqlite import connect, IntegrityError
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata import string_to_authors, authors_to_string, MetaInformation
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata, set_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
@ -556,7 +557,8 @@ class LibraryDatabase2(LibraryDatabase):
traceback.print_exc() traceback.print_exc()
continue continue
def cover(self, index, index_is_id=False, as_file=False, as_image=False): def cover(self, index, index_is_id=False, as_file=False, as_image=False,
as_path=False):
''' '''
Return the cover image as a bytestring (in JPEG format) or None. Return the cover image as a bytestring (in JPEG format) or None.
@ -566,6 +568,8 @@ class LibraryDatabase2(LibraryDatabase):
id = index if index_is_id else self.id(index) id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg') path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
if as_path:
return path
f = open(path, 'rb') f = open(path, 'rb')
if as_image: if as_image:
img = QImage() img = QImage()
@ -573,6 +577,30 @@ class LibraryDatabase2(LibraryDatabase):
return img return img
return f if as_file else f.read() return f if as_file else f.read()
def get_metadata(self, idx, index_is_id=False, get_cover=False):
'''
Convenience method to return metadata as a L{MetaInformation} object.
'''
aum = self.authors(idx, index_is_id=index_is_id)
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
tags = self.tags(idx, index_is_id=index_is_id)
if tags:
mi.tags = [i.strip() for i in tags.split(',')]
mi.series = self.series(idx, index_is_id=index_is_id)
if mi.series:
mi.series_index = self.series_index(idx, index_is_id=index_is_id)
mi.rating = self.rating(idx, index_is_id=index_is_id)
mi.isbn = self.isbn(idx, index_is_id=index_is_id)
id = idx if index_is_id else self.id(idx)
mi.application_id = id
if get_cover:
mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi
def has_book(self, mi): def has_book(self, mi):
title = mi.title title = mi.title
if title: if title:
@ -1322,5 +1350,107 @@ books_series_link feeds
self.vacuum() self.vacuum()
progress.reset() progress.reset()
return len(books) return len(books)
def export_to_dir(self, dir, indices, byauthor=False, single_dir=False,
index_is_id=False, callback=None):
if not os.path.exists(dir):
raise IOError('Target directory does not exist: '+dir)
by_author = {}
count = 0
for index in indices:
id = index if index_is_id else self.id(index)
au = self.conn.get('SELECT author_sort FROM books WHERE id=?',
(id,), all=False)
if not au:
au = self.authors(index, index_is_id=index_is_id)
if not au:
au = _('Unknown')
au = au.split(',')[0]
if not by_author.has_key(au):
by_author[au] = []
by_author[au].append(index)
for au in by_author.keys():
apath = os.path.join(dir, sanitize_file_name(au))
if not single_dir and not os.path.exists(apath):
os.mkdir(apath)
for idx in by_author[au]:
title = re.sub(r'\s', ' ', self.title(idx, index_is_id=index_is_id))
tpath = os.path.join(apath, sanitize_file_name(title))
id = idx if index_is_id else self.id(idx)
id = str(id)
if not single_dir and not os.path.exists(tpath):
os.mkdir(tpath)
name = au + ' - ' + title if byauthor else title + ' - ' + au
name += '_'+id
base = dir if single_dir else tpath
mi = self.get_metadata(idx, index_is_id=index_is_id, get_cover=True)
f = open(os.path.join(base, sanitize_file_name(name)+'.opf'), 'wb')
if not mi.authors:
mi.authors = [_('Unknown')]
opf = OPFCreator(base, mi)
opf.render(f)
f.close()
fmts = self.formats(idx, index_is_id=index_is_id)
if not fmts:
fmts = ''
for fmt in fmts.split(','):
data = self.format(idx, fmt, index_is_id=index_is_id)
if not data:
continue
fname = name +'.'+fmt.lower()
fname = sanitize_file_name(fname)
f = open(os.path.join(base, fname), 'w+b')
f.write(data)
f.flush()
f.seek(0)
try:
set_metadata(f, mi, fmt.lower())
except:
pass
f.close()
count += 1
if callable(callback):
if not callback(count, mi.title):
return
def export_single_format_to_dir(self, dir, indices, format,
index_is_id=False, callback=None):
dir = os.path.abspath(dir)
if not index_is_id:
indices = map(self.id, indices)
failures = []
for count, id in enumerate(indices):
try:
data = self.format(id, format, index_is_id=True)
if not data:
failures.append((id, self.title(id, index_is_id=True)))
continue
except:
failures.append((id, self.title(id, index_is_id=True)))
continue
title = self.title(id, index_is_id=True)
au = self.authors(id, index_is_id=True)
if not au:
au = _('Unknown')
fname = '%s - %s.%s'%(title, au, format.lower())
fname = sanitize_file_name(fname)
if not os.path.exists(dir):
os.makedirs(dir)
f = open(os.path.join(dir, fname), 'w+b')
f.write(data)
f.seek(0)
try:
set_metadata(f, self.get_metadata(id, index_is_id=True, get_cover=True),
stream_type=format.lower())
except:
pass
f.close()
if callable(callback):
if not callback(count, title):
break
return failures