From f8aa3bf8de8350331721ebb4dccf7d7630ec6bdc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 8 Feb 2009 17:10:53 -0800 Subject: [PATCH] Set cover in MOBI files when saving to disk. Also update metadata in LRF, EPUB and MOBI files when sending to device. --- src/calibre/ebooks/metadata/epub.py | 2 +- src/calibre/ebooks/metadata/mobi.py | 34 ++++--- src/calibre/ebooks/mobi/langcodes.py | 16 ++-- src/calibre/gui2/library.py | 6 +- src/calibre/gui2/main.py | 3 +- src/calibre/library/database.py | 113 +--------------------- src/calibre/library/database2.py | 138 ++++++++++++++++++++++++++- 7 files changed, 173 insertions(+), 139 deletions(-) diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py index 360869cc9c..18e68e4b32 100644 --- a/src/calibre/ebooks/metadata/epub.py +++ b/src/calibre/ebooks/metadata/epub.py @@ -18,7 +18,7 @@ from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup from calibre.ebooks.metadata import get_parser, MetaInformation from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory -from calibre import CurrentDir, fit_image +from calibre import CurrentDir class EPubException(Exception): pass diff --git a/src/calibre/ebooks/metadata/mobi.py b/src/calibre/ebooks/metadata/mobi.py index d31e19a03e..815b293c42 100644 --- a/src/calibre/ebooks/metadata/mobi.py +++ b/src/calibre/ebooks/metadata/mobi.py @@ -23,8 +23,8 @@ class StreamSlicer(object): def __init__(self, stream, start=0, stop=None): self._stream = stream self.start = start - if stop is None: - stream.seek(0, 2) + if stop is None: + stream.seek(0, 2) stop = stream.tell() self.stop = stop self._len = stop - start @@ -74,7 +74,7 @@ class StreamSlicer(object): raise TypeError("stream indices must be integers") -class MetadataUpdater(object): +class MetadataUpdater(object): def __init__(self, stream): self.stream = stream data = self.data = StreamSlicer(stream) @@ -151,18 +151,22 @@ class MetadataUpdater(object): self.exth[:] = ''.join([exth, title, '\0' * trail]) self.record0[84:92] = pack('>II', title_off, title_len) self.record0[92:96] = iana2mobi(mi.language) - if mi.cover_data[1]: - data = mi.cover_data[1] - if self.cover_record is not None: - size = len(self.cover_record) - cover = rescale_image(data, size) - cover += '\0' * (size - len(cover)) - self.cover_record[:] = cover - 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 + if mi.cover_data[1] or mi.cover: + try: + data = mi.cover_data[1] if mi.cover_data[1] else open(mi.cover, 'rb').read() + except: + pass + else: + if self.cover_record is not None: + size = len(self.cover_record) + cover = rescale_image(data, size) + cover += '\0' * (size - len(cover)) + self.cover_record[:] = cover + 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 def set_metadata(stream, mi): diff --git a/src/calibre/ebooks/mobi/langcodes.py b/src/calibre/ebooks/mobi/langcodes.py index 5df11c4e38..da0b3ef62e 100644 --- a/src/calibre/ebooks/mobi/langcodes.py +++ b/src/calibre/ebooks/mobi/langcodes.py @@ -306,13 +306,15 @@ IANA_MOBI = \ 'zu': {None: (53, 0)}} def iana2mobi(icode): - subtags = list(icode.split('-')) - langdict = IANA_MOBI[None] - while len(subtags) > 0: - lang = subtags.pop(0).lower() - if lang in IANA_MOBI: - langdict = IANA_MOBI[lang] - break + langdict, subtags = IANA_MOBI[None], [] + if icode: + subtags = list(icode.split('-')) + while len(subtags) > 0: + lang = subtags.pop(0).lower() + if lang in IANA_MOBI: + langdict = IANA_MOBI[lang] + break + mcode = langdict[None] while len(subtags) > 0: subtag = subtags.pop(0) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 2b0148a194..9f0877ca09 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -18,6 +18,7 @@ from calibre.library.database2 import FIELD_MAP 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 class LibraryDelegate(QItemDelegate): 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 = [] for row in (row.row() for row in rows): format = None @@ -441,6 +442,9 @@ class BooksModel(QAbstractTableModel): pt = PersistentTemporaryFile(suffix='.'+format) pt.write(self.db.format(row, format)) 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) ans.append(pt) else: diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 1fda4100df..3649a2264b 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -932,7 +932,8 @@ class Main(MainWindow, Ui_MainWindow): mi['cover'] = self.cover_to_thumbnail(cdata) metadata = iter(metadata) _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] bad, good, gf, names, remove_ids = [], [], [], [], [] for f in files: diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 2979c95d26..37fdeb4ce4 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -4,12 +4,11 @@ __copyright__ = '2008, Kovid Goyal ' Backend that implements storage of ebooks in an sqlite database. ''' 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 calibre import sanitize_file_name 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 import BOOK_EXTENSIONS 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): 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): @@ -1573,43 +1502,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; 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): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 4a2c669a25..8c762f8680 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -19,8 +19,9 @@ from calibre.library import title_sort from calibre.library.database import LibraryDatabase from calibre.library.sqlite import connect, IntegrityError from calibre.utils.search_query_parser import SearchQueryParser -from calibre.ebooks.metadata import string_to_authors, authors_to_string -from calibre.ebooks.metadata.meta import get_metadata +from calibre.ebooks.metadata import string_to_authors, authors_to_string, MetaInformation +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.ptempfile import PersistentTemporaryFile from calibre.customize.ui import run_plugins_on_import @@ -556,7 +557,8 @@ class LibraryDatabase2(LibraryDatabase): traceback.print_exc() 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. @@ -566,6 +568,8 @@ class LibraryDatabase2(LibraryDatabase): 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') if os.access(path, os.R_OK): + if as_path: + return path f = open(path, 'rb') if as_image: img = QImage() @@ -573,6 +577,30 @@ class LibraryDatabase2(LibraryDatabase): return img 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): title = mi.title if title: @@ -1322,5 +1350,107 @@ books_series_link feeds self.vacuum() progress.reset() 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 + +