From 04090c8e6fa31129a492075df99afb76278c861e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 2 May 2013 18:46:14 +0530 Subject: [PATCH] Implement set_cover() and set_metada(), tests still to come --- src/calibre/db/backend.py | 18 ++++++ src/calibre/db/cache.py | 101 +++++++++++++++++++++++++++++++- src/calibre/db/tests/writing.py | 3 + 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index d0ded25954..917178ab18 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -25,6 +25,7 @@ from calibre.utils.config import to_json, from_json, prefs, tweaks from calibre.utils.date import utcfromtimestamp, parse_date from calibre.utils.filenames import (is_case_sensitive, samefile, hardlink_file, ascii_filename, WindowsAtomicFolderMove) +from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.recycle_bin import delete_tree from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable, SizeTable, FormatsTable, AuthorsTable, IdentifiersTable, PathTable, @@ -973,6 +974,23 @@ class DB(object): return True return False + def set_cover(self, book_id, path, data): + path = os.path.abspath(os.path.join(self.library_path, path)) + if not os.path.exists(path): + os.makedirs(path) + path = os.path.join(path, 'cover.jpg') + if callable(getattr(data, 'save', None)): + from calibre.gui2 import pixmap_to_data + data = pixmap_to_data(data) + else: + if callable(getattr(data, 'read', None)): + data = data.read() + try: + save_cover_data_to(data, path) + except (IOError, OSError): + time.sleep(0.2) + save_cover_data_to(data, path) + def copy_format_to(self, book_id, fmt, fname, path, dest, windows_atomic_move=None, use_hardlink=False): path = self.format_abspath(book_id, fmt, fname, path) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 7c2176ec31..ba68de23a7 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -22,6 +22,7 @@ from calibre.db.search import Search from calibre.db.tables import VirtualTable from calibre.db.write import get_series_values from calibre.db.lazy import FormatMetadata, FormatsList +from calibre.ebooks.metadata import string_to_authors from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ptempfile import (base_dir, PersistentTemporaryFile, @@ -669,7 +670,7 @@ class Cache(object): self.dirtied_cache.update(new_dirtied) @write_api - def set_field(self, name, book_id_to_val_map, allow_case_change=True): + def set_field(self, name, book_id_to_val_map, allow_case_change=True, do_path_update=True): f = self.fields[name] is_series = f.metadata['datatype'] == 'series' update_path = name in {'title', 'authors'} @@ -702,7 +703,7 @@ class Cache(object): for name in self.composites: self.fields[name].pop_cache(dirtied) - if dirtied and update_path: + if dirtied and update_path and do_path_update: self._update_path(dirtied, mark_as_dirtied=False) self._mark_as_dirty(dirtied) @@ -822,6 +823,102 @@ class Cache(object): if callback is not None: callback(book_id, mi, True) + @write_api + def set_cover(self, book_id_data_map): + ''' Set the cover for this book. data can be either a QImage, + QPixmap, file object or bytestring ''' + + for book_id, data in book_id_data_map.iteritems(): + try: + path = self._field_for('path', book_id).replace('/', os.sep) + except AttributeError: + self._update_path((book_id,)) + path = self._field_for('path', book_id).replace('/', os.sep) + + self.backend.set_cover(book_id, path, data) + self._set_field('cover', {book_id:1 for book_id in book_id_data_map}) + + @write_api + def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False, + set_title=True, set_authors=True): + if callable(getattr(mi, 'to_book_metadata', None)): + # Handle code passing in an OPF object instead of a Metadata object + mi = mi.to_book_metadata() + + def set_field(name, val, **kwargs): + self._set_field(name, {book_id:val}, **kwargs) + + path_changed = False + if set_title and mi.title: + path_changed = True + set_field('title', mi.title, do_path_update=False) + if set_authors: + path_changed = True + if not mi.authors: + mi.authors = [_('Unknown')] + authors = [] + for a in mi.authors: + authors += string_to_authors(a) + set_field('authors', authors, do_path_update=False) + + if path_changed: + self._update_path((book_id,)) + + def protected_set_field(name, val, **kwargs): + try: + set_field(name, val, **kwargs) + except: + if ignore_errors: + traceback.print_exc() + else: + raise + + for field in ('rating', 'series_index', 'timestamp'): + val = getattr(mi, field) + if val is not None: + protected_set_field(field, val) + + # force_changes has no effect on cover manipulation + cdata = mi.cover_data[1] + if cdata is None and isinstance(mi.cover, basestring) and mi.cover and os.access(mi.cover, os.R_OK): + with lopen(mi.cover, 'rb') as f: + raw = f.read() + if raw: + cdata = raw + if cdata is not None: + self._set_cover({book_id: cdata}) + + for field in ('title_sort', 'author_sort', 'publisher', 'series', + 'tags', 'comments', 'languages', 'pubdate'): + val = mi.get(field, None) + if (force_changes and val is not None) or not mi.is_null(field): + protected_set_field(field, val) + + # identifiers will always be replaced if force_changes is True + mi_idents = mi.get_identifiers() + if force_changes: + protected_set_field('identifiers', mi_idents) + elif mi_idents: + identifiers = self._field_for('identifiers', book_id, default_value={}) + for key, val in mi_idents.iteritems(): + if val and val.strip(): # Don't delete an existing identifier + identifiers[icu_lower(key)] = val + protected_set_field('identifiers', identifiers) + + user_mi = mi.get_all_user_metadata(make_copy=False) + fm = self.field_metadata + for key in user_mi.iterkeys(): + if (key in fm and + user_mi[key]['datatype'] == fm[key]['datatype'] and + (user_mi[key]['datatype'] != 'text' or + user_mi[key]['is_multiple'] == fm[key]['is_multiple'])): + val = mi.get(key, None) + if force_changes or val is not None: + protected_set_field(key, val) + extra = mi.get_extra(key) + if extra is not None: + protected_set_field(key+'_index', extra) + # }}} class SortKey(object): # {{{ diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index f4fac34a57..b49611a347 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -355,4 +355,7 @@ class WritingTest(BaseTest): ae(opf.authors, ['author1', 'author2']) # }}} + def test_set_cover(self): + ' Test setting of cover ' + # TODO: test set_cover() and set_metadata()