'Set metadata for the book `id` from the `Metadata` object `mi`
Setting force_changes=True will force set_metadata to update fields even
if mi contains empty values. In this case, \'None\' is distinguished from
\'empty\'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
The tags, identifiers, and cover attributes are special cases. Tags and
identifiers cannot be set to None so they will always be replaced if
force_changes is true. You must ensure that mi contains the values you
want the book to have. Covers are always changed if a new cover is
provided, but are never deleted. Also note that force_changes has no
effect on setting title or authors.'

@write_api
def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False):

    dirtied = set()
    try:
        mi = mi.to_book_metadata()
    except (AttributeError, TypeError):
        pass
    def set_field(name, val):
        dirtied.update(self._set_field(name, {book_id: val}, do_path_update=False, allow_case_change=allow_case_change))
    path_changed = False
    if (set_title and mi.title):
        path_changed = True
        set_field(u'title', mi.title)
    if set_authors:
        path_changed = True
        if (not mi.authors):
            mi.authors = [_(u'Unknown')]
        authors = []
        for a in mi.authors:
            authors += string_to_authors(a)
        set_field(u'authors', authors)
    if path_changed:
        self._update_path({book_id})
    def protected_set_field(name, val):
        try:
            set_field(name, val)
        except:
            if ignore_errors:
                traceback.print_exc()
            else:
                raise
    try:
        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, u'rb') as f:
                cdata = (f.read() or None)
        if (cdata is not None):
            self._set_cover({book_id: cdata})
    except:
        if ignore_errors:
            traceback.print_exc()
        else:
            raise
    try:
        with self.backend.conn:
            for field in (u'rating', u'series_index', u'timestamp'):
                val = getattr(mi, field)
                if (val is not None):
                    protected_set_field(field, val)
            for field in (u'author_sort', u'publisher', u'series', u'tags', u'comments', u'languages', u'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)
            val = mi.get(u'title_sort', None)
            if ((force_changes and (val is not None)) or (not mi.is_null(u'title_sort'))):
                protected_set_field(u'sort', val)
            mi_idents = mi.get_identifiers()
            if force_changes:
                protected_set_field(u'identifiers', mi_idents)
            elif mi_idents:
                identifiers = self._field_for(u'identifiers', book_id, default_value={})
                for (key, val) in mi_idents.iteritems():
                    if (val and val.strip()):
                        identifiers[icu_lower(key)] = val
                protected_set_field(u'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][u'datatype'] == fm[key][u'datatype']) and ((user_mi[key][u'datatype'] != u'text') or (user_mi[key][u'is_multiple'] == fm[key][u'is_multiple']))):
                    val = mi.get(key, None)
                    if (force_changes or (val is not None)):
                        protected_set_field(key, val)
                        idx = (key + u'_index')
                        if (idx in self.fields):
                            extra = mi.get_extra(key)
                            if ((extra is not None) or force_changes):
                                protected_set_field(idx, extra)
    except:
        self._reload_from_db()
        raise
    return dirtied
