def textbox_changed(self):
     cur_text = unicode(self.textbox.toPlainText())
     if self.last_text != cur_text:
         self.last_text = cur_text
         self.highlighter.regenerate_paren_positions()
         self.text_cursor_changed()
         self.template_value.setText(
             SafeFormat().safe_format(cur_text, self.mi,
                                             _('EXCEPTION: '), self.mi))
Beispiel #2
0
 def __init__(self, db, book_id, formatter=None):
     sa(self, 'template_cache', db.formatter_template_cache)
     sa(self, 'formatter', SafeFormat() if formatter is None else formatter)
     sa(self, '_db', weakref.ref(db))
     sa(self, '_book_id', book_id)
     sa(self, '_cache', {
         'cover_data': (None, None),
         'device_collections': []
     })
     sa(self, '_user_metadata', db.field_metadata)
 def display_values(self, txt):
     tv = self.template_value
     l = self.template_value.selectionModel().selectedRows()
     break_on_mi = 0 if len(l) == 0 else l[0].row()
     for r, mi in enumerate(self.mi):
         w = tv.cellWidget(r, 0)
         w.setText(mi.title)
         w.setCursorPosition(0)
         v = SafeFormat().safe_format(txt,
                                      mi,
                                      _('EXCEPTION: '),
                                      mi,
                                      global_vars=self.global_vars,
                                      template_functions=self.all_functions,
                                      break_reporter=self.break_reporter
                                      if r == break_on_mi else None)
         w = tv.cellWidget(r, 1)
         w.setText(v.translate(translate_table))
         w.setCursorPosition(0)
Beispiel #4
0
 def display_values(self, txt):
     tv = self.template_value
     for r,mi in enumerate(self.mi):
         w = tv.cellWidget(r, 0)
         w.setText(mi.title)
         w.setCursorPosition(0)
         v = SafeFormat().safe_format(txt, mi, _('EXCEPTION: '),
                                      mi, global_vars=self.global_vars,
                                      template_functions=self.all_functions)
         w = tv.cellWidget(r, 1)
         w.setText(v)
         w.setCursorPosition(0)
Beispiel #5
0
 def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None,
              formatter=None):
     '''
     @param title: title or ``_('Unknown')``
     @param authors: List of strings or []
     @param other: None or a metadata object
     '''
     _data = copy.deepcopy(NULL_VALUES)
     _data.pop('language')
     object.__setattr__(self, '_data', _data)
     if other is not None:
         self.smart_update(other)
     else:
         if title:
             self.title = title
         if authors:
             # List of strings or []
             self.author = list(authors) if authors else []  # Needed for backward compatibility
             self.authors = list(authors) if authors else []
     from calibre.ebooks.metadata.book.formatter import SafeFormat
     self.formatter = SafeFormat() if formatter is None else formatter
     self.template_cache = template_cache
Beispiel #6
0
 def template_to_attribute(self, other, ops):
     '''
     Takes a list [(src,dest), (src,dest)], evaluates the template in the
     context of other, then copies the result to self[dest]. This is on a
     best-efforts basis. Some assignments can make no sense.
     '''
     if not ops:
         return
     from calibre.ebooks.metadata.book.formatter import SafeFormat
     formatter = SafeFormat()
     for op in ops:
         try:
             src = op[0]
             dest = op[1]
             val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
             if dest == 'tags':
                 self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
             elif dest == 'authors':
                 self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
             else:
                 self.set(dest, val)
         except:
             if DEBUG:
                 traceback.print_exc()
Beispiel #7
0
 def template_to_attribute(self, other, ops):
     '''
     Takes a list [(src,dest), (src,dest)], evaluates the template in the
     context of other, then copies the result to self[dest]. This is on a
     best-efforts basis. Some assignments can make no sense.
     '''
     if not ops:
         return
     from calibre.ebooks.metadata.book.formatter import SafeFormat
     formatter = SafeFormat()
     for op in ops:
         try:
             src = op[0]
             dest = op[1]
             val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
             if dest == 'tags':
                 self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
             elif dest == 'authors':
                 self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
             else:
                 self.set(dest, val)
         except:
             if DEBUG:
                 traceback.print_exc()
Beispiel #8
0
 def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None):
     '''
     @param title: title or ``_('Unknown')``
     @param authors: List of strings or []
     @param other: None or a metadata object
     '''
     _data = copy.deepcopy(NULL_VALUES)
     _data.pop('language')
     object.__setattr__(self, '_data', _data)
     if other is not None:
         self.smart_update(other)
     else:
         if title:
             self.title = title
         if authors:
             # List of strings or []
             self.author = list(authors) if authors else []# Needed for backward compatibility
             self.authors = list(authors) if authors else []
     from calibre.ebooks.metadata.book.formatter import SafeFormat
     self.formatter = SafeFormat()
     self.template_cache = template_cache
def get_title_author_series(mi, options=None):
    if not options:
        options = cfg.plugin_prefs[cfg.STORE_CURRENT]
    title = normalize(mi.title)
    authors = mi.authors
    if options.get(cfg.KEY_SWAP_AUTHOR, False):
        swapped_authors = []
        for author in authors:
            swapped_authors.append(swap_author_names(author))
        authors = swapped_authors
    author_string = normalize(authors_to_string(authors))

    series = None
    if mi.series:
        series_text = options.get(cfg.KEY_SERIES_TEXT, '')
        if not series_text:
            series_text = cfg.DEFAULT_SERIES_TEXT
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        series = SafeFormat().safe_format(
            series_text, mi, 'GC template error', mi)
    series_string = normalize(series)
    return (title, author_string, series_string)
Beispiel #10
0
 def s_r_get_field(self, mi, field):
     if field:
         if field == '{template}':
             v = SafeFormat().safe_format(unicode(self.s_r_template.text()),
                                          mi, _('S/R TEMPLATE ERROR'), mi)
             return [v]
         fm = self.db.metadata_for_field(field)
         if field == 'sort':
             val = mi.get('title_sort', None)
         else:
             val = mi.get(field, None)
         if isinstance(val, (int, float, bool)):
             val = str(val)
         elif fm['is_csp']:
             # convert the csp dict into a list
             id_type = unicode(self.s_r_src_ident.currentText())
             if id_type:
                 val = [val.get(id_type, '')]
             else:
                 val = [u'%s:%s' % (t[0], t[1]) for t in val.iteritems()]
         if val is None:
             val = [] if fm['is_multiple'] else ['']
         elif not fm['is_multiple']:
             val = [val]
         elif fm['datatype'] == 'composite':
             val = [
                 v2.strip()
                 for v2 in val.split(fm['is_multiple']['ui_to_list'])
             ]
         elif field == 'authors':
             val = [v2.replace('|', ',') for v2 in val]
     else:
         val = []
     if not val:
         val = ['']
     return val
def generate_cover_for_book(mi, options=None, db=None):
    if not options:
        options = cfg.plugin_prefs[cfg.STORE_CURRENT]
    title, author_string, series_string = get_title_author_series(mi, options)
    custom_text = options.get(cfg.KEY_CUSTOM_TEXT, None)
    if custom_text:
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        custom_text = SafeFormat().safe_format(
            custom_text.replace('\n', '<br/>'), mi, 'GC template error', mi)

    fonts = options[cfg.KEY_FONTS]
    margin = options[cfg.KEY_MARGINS]['text']
    content_lines = {}
    content_lines['Title'] = [
        get_textline(title_line.strip(), fonts['title'], margin)
        for title_line in split_and_replace_newlines(title)]
    content_lines['Author'] = [
        get_textline(author_line.strip(), fonts['author'], margin)
        for author_line in split_and_replace_newlines(author_string)]
    if series_string:
        content_lines['Series'] = [
            get_textline(series_line.strip(), fonts['series'], margin)
            for series_line in split_and_replace_newlines(series_string)]
    if custom_text:
        content_lines['Custom Text'] = [
            get_textline(ct.strip(), fonts['custom'], margin)
            for ct in split_and_replace_newlines(custom_text)]
    top_lines = []
    bottom_lines = []
    field_order = options[cfg.KEY_FIELD_ORDER]
    above_image = True
    display_image = False
    for field in field_order:
        field_name = field['name']
        if field_name == 'Image':
            display_image = field['display']
            above_image = False
            continue
        if field_name not in content_lines:
            continue
        if field['display']:
            lines = content_lines[field_name]
            for line in lines:
                if above_image:
                    top_lines.append(line)
                else:
                    bottom_lines.append(line)

    image_name = options[cfg.KEY_IMAGE_FILE]
    image_path = None
    if image_name == cfg.TOKEN_CURRENT_COVER:
        image_path = getattr(mi, '_path_to_cover', None)
        if not image_path and db:
            image_path = mi._path_to_cover = db.cover(mi.id, as_path=True)
    elif image_name == cfg.TOKEN_DEFAULT_COVER:
        image_path = I('library.png')  # noqa
    else:
        image_path = os.path.join(cfg.get_images_dir(), image_name)
    if image_path is None or not os.path.exists(image_path):
        image_path = I('library.png')  # noqa
    return create_cover_page(
        top_lines, bottom_lines, display_image, options, image_path)
Beispiel #12
0
 def get_value(self, orig_key, args, kwargs):
     ans = SafeFormat.get_value(self, orig_key, args, kwargs)
     return escape_formatting(ans)
Beispiel #13
0
    def _do_split(self,
                  db,
                  source_id,
                  misource,
                  splitepub,
                  newspecs,
                  deftitle=None,
                  editmeta=True):

        linenums, changedtocs = newspecs
        # logger.debug("updated tocs:%s"%changedtocs)

        # logger.debug("2:%s"%(time.time()-self.t))
        self.t = time.time()

        # logger.debug("linenums:%s"%linenums)

        defauthors = None

        if not deftitle and prefs['copytitle']:
            deftitle = _("نمونه %s") % misource.title

        if prefs['copyauthors']:
            defauthors = misource.authors

        mi = MetaInformation(deftitle, defauthors)

        if prefs['copytags']:
            mi.tags = misource.tags  # [item for sublist in tagslists for item in sublist]

        if prefs['copylanguages']:
            mi.languages = misource.languages

        if prefs['copyseries']:
            mi.series = misource.series

        if prefs['copydate']:
            mi.timestamp = misource.timestamp

        if prefs['copyrating']:
            mi.rating = misource.rating

        if prefs['copypubdate']:
            mi.pubdate = misource.pubdate

        if prefs['copypublisher']:
            mi.publisher = misource.publisher

        if prefs['copyidentifiers']:
            mi.set_identifiers(misource.get_identifiers())

        if prefs['copycomments'] and misource.comments:
            mi.comments = _("Split from:") + "\n\n" + misource.comments

        # logger.debug("mi:%s"%mi)
        book_id = db.create_book_entry(mi,
                                       add_duplicates=True)

        if prefs['copycover'] and misource.has_cover:
            db.set_cover(book_id, db.cover(source_id, index_is_id=True))

        # logger.debug("3:%s"%(time.time()-self.t))
        self.t = time.time()

        custom_columns = self.gui.library_view.model().custom_columns
        for col, action in prefs['custom_cols'].iteritems():
            # logger.debug("col: %s action: %s"%(col,action))

            if col not in custom_columns:
                # logger.debug("%s not an existing column, skipping."%col)
                continue

            coldef = custom_columns[col]
            # logger.debug("coldef:%s"%coldef)
            label = coldef['label']
            value = db.get_custom(source_id, label=label, index_is_id=True)
            if value:
                db.set_custom(book_id, value, label=label, commit=False)

        # logger.debug("3.5:%s"%(time.time()-self.t))
        self.t = time.time()

        if prefs['sourcecol'] != '' and prefs['sourcecol'] in custom_columns \
                and prefs['sourcetemplate']:
            val = SafeFormat().safe_format(prefs['sourcetemplate'], misource, 'EpubSplit Source Template Error',
                                           misource)
            # logger.debug("Attempting to set %s to %s"%(prefs['sourcecol'],val))
            label = custom_columns[prefs['sourcecol']]['label']
            db.set_custom(book_id, val, label=label, commit=False)

        db.commit()

        # logger.debug("4:%s"%(time.time()-self.t))
        self.t = time.time()

        self.gui.library_view.model().books_added(1)
        self.gui.library_view.select_rows([book_id])

        # logger.debug("5:%s"%(time.time()-self.t))
        self.t = time.time()

        # if editmeta:
        #     confirm('\n'+_('کتاب نمونه ساخته شود؟')+'\n',
        #             'epubsplit_created_now_edit_again',
        #             self.gui)
        #
        #     self.gui.iactions['Edit Metadata'].edit_metadata(False)

        # logger.debug("5:%s"%(time.time()-self.t))
        self.t = time.time()
        self.gui.tags_view.recount()

        self.gui.status_bar.show_message(_('فایل نمونه ساخته شد'), 60000)

        mi = db.get_metadata(book_id, index_is_id=True)

        outputepub = PersistentTemporaryFile(suffix='.epub')

        coverjpgpath = None
        # if mi.has_cover:
        #     # grab the path to the real image.
        #     coverjpgpath = os.path.join(db.library_path, db.path(book_id, index_is_id=True), 'cover.jpg')

        splitepub.write_split_epub(outputepub,
                                   linenums,
                                   changedtocs=changedtocs,
                                   authoropts=mi.authors,
                                   titleopt=mi.title,
                                   descopt=mi.comments,
                                   tags=mi.tags,
                                   languages=mi.languages,
                                   coverjpgpath=coverjpgpath)

        # logger.debug("6:%s"%(time.time()-self.t))
        self.t = time.time()
        db.add_format_with_hooks(book_id,
                                 'EPUB',
                                 outputepub, index_is_id=True)

        # logger.debug("7:%s"%(time.time()-self.t))
        self.t = time.time()

        self.gui.status_bar.show_message(_('Finished splitting off EPUB.'), 3000)
        self.gui.library_view.model().refresh_ids([book_id])
        self.gui.tags_view.recount()
        current = self.gui.library_view.currentIndex()
        self.gui.library_view.model().current_changed(current, self.previous)
Beispiel #14
0
def formatter():
    global _formatter
    if _formatter is None:
        _formatter = SafeFormat()
    return _formatter
Beispiel #15
0
class Metadata(object):

    '''
    A class representing all the metadata for a book. The various standard metadata
    fields are available as attributes of this object. You can also stick
    arbitrary attributes onto this object.

    Metadata from custom columns should be accessed via the get() method,
    passing in the lookup name for the column, for example: "#mytags".

    Use the :meth:`is_null` method to test if a field is null.

    This object also has functions to format fields into strings.

    The list of standard metadata fields grows with time is in
    :data:`STANDARD_METADATA_FIELDS`.

    Please keep the method based API of this class to a minimum. Every method
    becomes a reserved field name.
    '''

    def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None,
                 formatter=None):
        '''
        @param title: title or ``_('Unknown')``
        @param authors: List of strings or []
        @param other: None or a metadata object
        '''
        _data = copy.deepcopy(NULL_VALUES)
        _data.pop('language')
        object.__setattr__(self, '_data', _data)
        if other is not None:
            self.smart_update(other)
        else:
            if title:
                self.title = title
            if authors:
                # List of strings or []
                self.author = list(authors) if authors else []  # Needed for backward compatibility
                self.authors = list(authors) if authors else []
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        self.formatter = SafeFormat() if formatter is None else formatter
        self.template_cache = template_cache

    def is_null(self, field):
        '''
        Return True if the value of field is null in this object.
        'null' means it is unknown or evaluates to False. So a title of
        _('Unknown') is null or a language of 'und' is null.

        Be careful with numeric fields since this will return True for zero as
        well as None.

        Also returns True if the field does not exist.
        '''
        try:
            null_val = NULL_VALUES.get(field, None)
            val = getattr(self, field, None)
            return not val or val == null_val
        except:
            return True

    def __getattribute__(self, field):
        _data = object.__getattribute__(self, '_data')
        if field in SIMPLE_GET:
            return _data.get(field, None)
        if field in TOP_LEVEL_IDENTIFIERS:
            return _data.get('identifiers').get(field, None)
        if field == 'language':
            try:
                return _data.get('languages', [])[0]
            except:
                return NULL_VALUES['language']
        try:
            return object.__getattribute__(self, field)
        except AttributeError:
            pass
        if field in _data['user_metadata'].iterkeys():
            d = _data['user_metadata'][field]
            val = d['#value#']
            if d['datatype'] != 'composite':
                return val
            if val is None:
                d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field
                val = d['#value#'] = self.formatter.safe_format(
                                            d['display']['composite_template'],
                                            self,
                                            _('TEMPLATE ERROR'),
                                            self, column_name=field,
                                            template_cache=self.template_cache).strip()
            return val
        if field.startswith('#') and field.endswith('_index'):
            try:
                return self.get_extra(field[:-6])
            except:
                pass
        raise AttributeError(
                'Metadata object has no attribute named: '+ repr(field))

    def __setattr__(self, field, val, extra=None):
        _data = object.__getattribute__(self, '_data')
        if field in SIMPLE_SET:
            if val is None:
                val = copy.copy(NULL_VALUES.get(field, None))
            _data[field] = val
        elif field in TOP_LEVEL_IDENTIFIERS:
            field, val = self._clean_identifier(field, val)
            identifiers = _data['identifiers']
            identifiers.pop(field, None)
            if val:
                identifiers[field] = val
        elif field == 'identifiers':
            if not val:
                val = copy.copy(NULL_VALUES.get('identifiers', None))
            self.set_identifiers(val)
        elif field == 'language':
            langs = []
            if val and val.lower() != 'und':
                langs = [val]
            _data['languages'] = langs
        elif field in _data['user_metadata'].iterkeys():
            _data['user_metadata'][field]['#value#'] = val
            _data['user_metadata'][field]['#extra#'] = extra
        else:
            # You are allowed to stick arbitrary attributes onto this object as
            # long as they don't conflict with global or user metadata names
            # Don't abuse this privilege
            self.__dict__[field] = val

    def __iter__(self):
        return object.__getattribute__(self, '_data').iterkeys()

    def has_key(self, key):
        return key in object.__getattribute__(self, '_data')

    def deepcopy(self, class_generator=lambda : Metadata(None)):
        ''' Do not use this method unless you know what you are doing, if you
        want to create a simple clone of this object, use :meth:`deepcopy_metadata`
        instead. Class_generator must be a function that returns an instance
        of Metadata or a subclass of it.'''
        m = class_generator()
        if not isinstance(m, Metadata):
            return None
        object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__))
        return m

    def deepcopy_metadata(self):
        m = Metadata(None)
        object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
        return m

    def get(self, field, default=None):
        try:
            return self.__getattribute__(field)
        except AttributeError:
            return default

    def get_extra(self, field, default=None):
        _data = object.__getattribute__(self, '_data')
        if field in _data['user_metadata'].iterkeys():
            try:
                return _data['user_metadata'][field]['#extra#']
            except:
                return default
        raise AttributeError(
                'Metadata object has no attribute named: '+ repr(field))

    def set(self, field, val, extra=None):
        self.__setattr__(field, val, extra)

    def get_identifiers(self):
        '''
        Return a copy of the identifiers dictionary.
        The dict is small, and the penalty for using a reference where a copy is
        needed is large. Also, we don't want any manipulations of the returned
        dict to show up in the book.
        '''
        ans = object.__getattribute__(self,
            '_data')['identifiers']
        if not ans:
            ans = {}
        return copy.deepcopy(ans)

    def _clean_identifier(self, typ, val):
        if typ:
            typ = ck(typ)
        if val:
            val = cv(val)
        return typ, val

    def set_identifiers(self, identifiers):
        '''
        Set all identifiers. Note that if you previously set ISBN, calling
        this method will delete it.
        '''
        cleaned = {ck(k):cv(v) for k, v in identifiers.iteritems() if k and v}
        object.__getattribute__(self, '_data')['identifiers'] = cleaned

    def set_identifier(self, typ, val):
        'If val is empty, deletes identifier of type typ'
        typ, val = self._clean_identifier(typ, val)
        if not typ:
            return
        identifiers = object.__getattribute__(self,
            '_data')['identifiers']

        identifiers.pop(typ, None)
        if val:
            identifiers[typ] = val

    def has_identifier(self, typ):
        identifiers = object.__getattribute__(self,
            '_data')['identifiers']
        return typ in identifiers

    # field-oriented interface. Intended to be the same as in LibraryDatabase

    def standard_field_keys(self):
        '''
        return a list of all possible keys, even if this book doesn't have them
        '''
        return STANDARD_METADATA_FIELDS

    def custom_field_keys(self):
        '''
        return a list of the custom fields in this book
        '''
        return object.__getattribute__(self, '_data')['user_metadata'].iterkeys()

    def all_field_keys(self):
        '''
        All field keys known by this instance, even if their value is None
        '''
        _data = object.__getattribute__(self, '_data')
        return frozenset(ALL_METADATA_FIELDS.union(_data['user_metadata'].iterkeys()))

    def metadata_for_field(self, key):
        '''
        return metadata describing a standard or custom field.
        '''
        if key not in self.custom_field_keys():
            return self.get_standard_metadata(key, make_copy=False)
        return self.get_user_metadata(key, make_copy=False)

    def all_non_none_fields(self):
        '''
        Return a dictionary containing all non-None metadata fields, including
        the custom ones.
        '''
        result = {}
        _data = object.__getattribute__(self, '_data')
        for attr in STANDARD_METADATA_FIELDS:
            v = _data.get(attr, None)
            if v is not None:
                result[attr] = v
        # separate these because it uses the self.get(), not _data.get()
        for attr in TOP_LEVEL_IDENTIFIERS:
            v = self.get(attr, None)
            if v is not None:
                result[attr] = v
        for attr in _data['user_metadata'].iterkeys():
            v = self.get(attr, None)
            if v is not None:
                result[attr] = v
                if _data['user_metadata'][attr]['datatype'] == 'series':
                    result[attr+'_index'] = _data['user_metadata'][attr]['#extra#']
        return result

    # End of field-oriented interface

    # Extended interfaces. These permit one to get copies of metadata dictionaries, and to
    # get and set custom field metadata

    def get_standard_metadata(self, field, make_copy):
        '''
        return field metadata from the field if it is there. Otherwise return
        None. field is the key name, not the label. Return a copy if requested,
        just in case the user wants to change values in the dict.
        '''
        if field in field_metadata and field_metadata[field]['kind'] == 'field':
            if make_copy:
                return copy.deepcopy(field_metadata[field])
            return field_metadata[field]
        return None

    def get_all_standard_metadata(self, make_copy):
        '''
        return a dict containing all the standard field metadata associated with
        the book.
        '''
        if not make_copy:
            return field_metadata
        res = {}
        for k in field_metadata:
            if field_metadata[k]['kind'] == 'field':
                res[k] = copy.deepcopy(field_metadata[k])
        return res

    def get_all_user_metadata(self, make_copy):
        '''
        return a dict containing all the custom field metadata associated with
        the book.
        '''
        _data = object.__getattribute__(self, '_data')
        user_metadata = _data['user_metadata']
        if not make_copy:
            return user_metadata
        res = {}
        for k in user_metadata:
            res[k] = copy.deepcopy(user_metadata[k])
        return res

    def get_user_metadata(self, field, make_copy):
        '''
        return field metadata from the object if it is there. Otherwise return
        None. field is the key name, not the label. Return a copy if requested,
        just in case the user wants to change values in the dict.
        '''
        _data = object.__getattribute__(self, '_data')
        _data = _data['user_metadata']
        if field in _data:
            if make_copy:
                return copy.deepcopy(_data[field])
            return _data[field]
        return None

    def set_all_user_metadata(self, metadata):
        '''
        store custom field metadata into the object. Field is the key name
        not the label
        '''
        if metadata is None:
            traceback.print_stack()
            return

        um = {}
        for key, meta in metadata.iteritems():
            m = meta.copy()
            if '#value#' not in m:
                if m['datatype'] == 'text' and m['is_multiple']:
                    m['#value#'] = []
                else:
                    m['#value#'] = None
            um[key] = m
        _data = object.__getattribute__(self, '_data')
        _data['user_metadata'] = um

    def set_user_metadata(self, field, metadata):
        '''
        store custom field metadata for one column into the object. Field is
        the key name not the label
        '''
        if field is not None:
            if not field.startswith('#'):
                raise AttributeError(
                        'Custom field name %s must begin with \'#\''%repr(field))
            if metadata is None:
                traceback.print_stack()
                return
            m = dict(metadata)
            # Copying the elements should not be necessary. The objects referenced
            # in the dict should not change. Of course, they can be replaced.
            # for k,v in metadata.iteritems():
            #     m[k] = copy.copy(v)
            if '#value#' not in m:
                if m['datatype'] == 'text' and m['is_multiple']:
                    m['#value#'] = []
                else:
                    m['#value#'] = None
            _data = object.__getattribute__(self, '_data')
            _data['user_metadata'][field] = m

    def template_to_attribute(self, other, ops):
        '''
        Takes a list [(src,dest), (src,dest)], evaluates the template in the
        context of other, then copies the result to self[dest]. This is on a
        best-efforts basis. Some assignments can make no sense.
        '''
        if not ops:
            return
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        formatter = SafeFormat()
        for op in ops:
            try:
                src = op[0]
                dest = op[1]
                val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other)
                if dest == 'tags':
                    self.set(dest, [f.strip() for f in val.split(',') if f.strip()])
                elif dest == 'authors':
                    self.set(dest, [f.strip() for f in val.split('&') if f.strip()])
                else:
                    self.set(dest, val)
            except:
                if DEBUG:
                    traceback.print_exc()

    # Old Metadata API {{{
    def print_all_attributes(self):
        for x in STANDARD_METADATA_FIELDS:
            prints('%s:'%x, getattr(self, x, 'None'))
        for x in self.custom_field_keys():
            meta = self.get_user_metadata(x, make_copy=False)
            if meta is not None:
                prints(x, meta)
        prints('--------------')

    def smart_update(self, other, replace_metadata=False):
        '''
        Merge the information in `other` into self. In case of conflicts, the information
        in `other` takes precedence, unless the information in `other` is NULL.
        '''
        def copy_not_none(dest, src, attr):
            v = getattr(src, attr, None)
            if v not in (None, NULL_VALUES.get(attr, None)):
                setattr(dest, attr, copy.deepcopy(v))

        if other.title and other.title != _('Unknown'):
            self.title = other.title
            if hasattr(other, 'title_sort'):
                self.title_sort = other.title_sort

        if other.authors and other.authors[0] != _('Unknown'):
            self.authors = list(other.authors)
            if hasattr(other, 'author_sort_map'):
                self.author_sort_map = dict(other.author_sort_map)
            if hasattr(other, 'author_sort'):
                self.author_sort = other.author_sort

        if replace_metadata:
            # SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
            for attr in SC_COPYABLE_FIELDS:
                setattr(self, attr, getattr(other, attr, 1.0 if
                        attr == 'series_index' else None))
            self.tags = other.tags
            self.cover_data = getattr(other, 'cover_data',
                                      NULL_VALUES['cover_data'])
            self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
            for x in SC_FIELDS_COPY_NOT_NULL:
                copy_not_none(self, other, x)
            if callable(getattr(other, 'get_identifiers', None)):
                self.set_identifiers(other.get_identifiers())
            # language is handled below
        else:
            for attr in SC_COPYABLE_FIELDS:
                copy_not_none(self, other, attr)
            for x in SC_FIELDS_COPY_NOT_NULL:
                copy_not_none(self, other, x)

            if other.tags:
                # Case-insensitive but case preserving merging
                lotags = [t.lower() for t in other.tags]
                lstags = [t.lower() for t in self.tags]
                ot, st = map(frozenset, (lotags, lstags))
                for t in st.intersection(ot):
                    sidx = lstags.index(t)
                    oidx = lotags.index(t)
                    self.tags[sidx] = other.tags[oidx]
                self.tags += [t for t in other.tags if t.lower() in ot-st]

            if getattr(other, 'cover_data', False):
                other_cover = other.cover_data[-1]
                self_cover = self.cover_data[-1] if self.cover_data else ''
                if not self_cover:
                    self_cover = ''
                if not other_cover:
                    other_cover = ''
                if len(other_cover) > len(self_cover):
                    self.cover_data = other.cover_data

            if callable(getattr(other, 'custom_field_keys', None)):
                for x in other.custom_field_keys():
                    meta = other.get_user_metadata(x, make_copy=True)
                    if meta is not None:
                        self_tags = self.get(x, [])
                        self.set_user_metadata(x, meta)  # get... did the deepcopy
                        other_tags = other.get(x, [])
                        if meta['datatype'] == 'text' and meta['is_multiple']:
                            # Case-insensitive but case preserving merging
                            lotags = [t.lower() for t in other_tags]
                            try:
                                lstags = [t.lower() for t in self_tags]
                            except TypeError:
                                # Happens if x is not a text, is_multiple field
                                # on self
                                lstags = []
                                self_tags = []
                            ot, st = map(frozenset, (lotags, lstags))
                            for t in st.intersection(ot):
                                sidx = lstags.index(t)
                                oidx = lotags.index(t)
                                self_tags[sidx] = other_tags[oidx]
                            self_tags += [t for t in other_tags if t.lower() in ot-st]
                            setattr(self, x, self_tags)

            my_comments = getattr(self, 'comments', '')
            other_comments = getattr(other, 'comments', '')
            if not my_comments:
                my_comments = ''
            if not other_comments:
                other_comments = ''
            if len(other_comments.strip()) > len(my_comments.strip()):
                self.comments = other_comments

            # Copy all the non-none identifiers
            if callable(getattr(other, 'get_identifiers', None)):
                d = self.get_identifiers()
                s = other.get_identifiers()
                d.update([v for v in s.iteritems() if v[1] is not None])
                self.set_identifiers(d)
            else:
                # other structure not Metadata. Copy the top-level identifiers
                for attr in TOP_LEVEL_IDENTIFIERS:
                    copy_not_none(self, other, attr)

        other_lang = getattr(other, 'languages', [])
        if other_lang and other_lang != ['und']:
            self.languages = list(other_lang)
        if not getattr(self, 'series', None):
            self.series_index = None

    def format_series_index(self, val=None):
        from calibre.ebooks.metadata import fmt_sidx
        v = self.series_index if val is None else val
        try:
            x = float(v)
        except (ValueError, TypeError):
            x = 1
        return fmt_sidx(x)

    def authors_from_string(self, raw):
        from calibre.ebooks.metadata import string_to_authors
        self.authors = string_to_authors(raw)

    def format_authors(self):
        from calibre.ebooks.metadata import authors_to_string
        return authors_to_string(self.authors)

    def format_tags(self):
        return u', '.join([unicode(t) for t in sorted(self.tags, key=sort_key)])

    def format_rating(self, v=None, divide_by=1.0):
        if v is None:
            if self.rating is not None:
                return unicode(self.rating/divide_by)
            return u'None'
        return unicode(v/divide_by)

    def format_field(self, key, series_with_index=True):
        '''
        Returns the tuple (display_name, formatted_value)
        '''
        name, val, ign, ign = self.format_field_extended(key, series_with_index)
        return (name, val)

    def format_field_extended(self, key, series_with_index=True):
        from calibre.ebooks.metadata import authors_to_string
        '''
        returns the tuple (display_name, formatted_value, original_value,
        field_metadata)
        '''
        from calibre.utils.date import format_date

        # Handle custom series index
        if key.startswith('#') and key.endswith('_index'):
            tkey = key[:-6]  # strip the _index
            cmeta = self.get_user_metadata(tkey, make_copy=False)
            if cmeta and cmeta['datatype'] == 'series':
                if self.get(tkey):
                    res = self.get_extra(tkey)
                    return (unicode(cmeta['name']+'_index'),
                            self.format_series_index(res), res, cmeta)
                else:
                    return (unicode(cmeta['name']+'_index'), '', '', cmeta)

        if key in self.custom_field_keys():
            res = self.get(key, None)       # get evaluates all necessary composites
            cmeta = self.get_user_metadata(key, make_copy=False)
            name = unicode(cmeta['name'])
            if res is None or res == '':    # can't check "not res" because of numeric fields
                return (name, res, None, None)
            orig_res = res
            datatype = cmeta['datatype']
            if datatype == 'text' and cmeta['is_multiple']:
                res = cmeta['is_multiple']['list_to_ui'].join(res)
            elif datatype == 'series' and series_with_index:
                if self.get_extra(key) is not None:
                    res = res + \
                        ' [%s]'%self.format_series_index(val=self.get_extra(key))
            elif datatype == 'datetime':
                res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
            elif datatype == 'bool':
                res = _('Yes') if res else _('No')
            elif datatype == 'rating':
                res = u'%.2g'%(res/2.0)
            elif datatype in ['int', 'float']:
                try:
                    fmt = cmeta['display'].get('number_format', None)
                    res = fmt.format(res)
                except:
                    pass
            return (name, unicode(res), orig_res, cmeta)

        # convert top-level ids into their value
        if key in TOP_LEVEL_IDENTIFIERS:
            fmeta = field_metadata['identifiers']
            name = key
            res = self.get(key, None)
            return (name, res, res, fmeta)

        # Translate aliases into the standard field name
        fmkey = field_metadata.search_term_to_field_key(key)
        if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
            res = self.get(key, None)
            fmeta = field_metadata[fmkey]
            name = unicode(fmeta['name'])
            if res is None or res == '':
                return (name, res, None, None)
            orig_res = res
            name = unicode(fmeta['name'])
            datatype = fmeta['datatype']
            if key == 'authors':
                res = authors_to_string(res)
            elif key == 'series_index':
                res = self.format_series_index(res)
            elif datatype == 'text' and fmeta['is_multiple']:
                if isinstance(res, dict):
                    res = [k + ':' + v for k,v in res.items()]
                res = fmeta['is_multiple']['list_to_ui'].join(sorted(filter(None, res), key=sort_key))
            elif datatype == 'series' and series_with_index:
                res = res + ' [%s]'%self.format_series_index()
            elif datatype == 'datetime':
                res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
            elif datatype == 'rating':
                res = u'%.2g'%(res/2.0)
            elif key == 'size':
                res = human_readable(res)
            return (name, unicode(res), orig_res, fmeta)

        return (None, None, None, None)

    def __unicode__(self):
        '''
        A string representation of this object, suitable for printing to
        console
        '''
        from calibre.utils.date import isoformat
        from calibre.ebooks.metadata import authors_to_string
        ans = []
        def fmt(x, y):
            ans.append(u'%-20s: %s'%(unicode(x), unicode(y)))

        fmt('Title', self.title)
        if self.title_sort:
            fmt('Title sort', self.title_sort)
        if self.authors:
            fmt('Author(s)',  authors_to_string(self.authors) +
               ((' [' + self.author_sort + ']')
                if self.author_sort and self.author_sort != _('Unknown') else ''))
        if self.publisher:
            fmt('Publisher', self.publisher)
        if getattr(self, 'book_producer', False):
            fmt('Book Producer', self.book_producer)
        if self.tags:
            fmt('Tags', u', '.join([unicode(t) for t in self.tags]))
        if self.series:
            fmt('Series', self.series + ' #%s'%self.format_series_index())
        if not self.is_null('languages'):
            fmt('Languages', ', '.join(self.languages))
        if self.rating is not None:
            fmt('Rating', (u'%.2g'%(float(self.rating)/2.0)) if self.rating
                    else u'')
        if self.timestamp is not None:
            fmt('Timestamp', isoformat(self.timestamp))
        if self.pubdate is not None:
            fmt('Published', isoformat(self.pubdate))
        if self.rights is not None:
            fmt('Rights', unicode(self.rights))
        if self.identifiers:
            fmt('Identifiers', u', '.join(['%s:%s'%(k, v) for k, v in
                self.identifiers.iteritems()]))
        if self.comments:
            fmt('Comments', self.comments)

        for key in self.custom_field_keys():
            val = self.get(key, None)
            if val:
                (name, val) = self.format_field(key)
                fmt(name, unicode(val))
        return u'\n'.join(ans)

    def to_html(self):
        '''
        A HTML representation of this object.
        '''
        from calibre.ebooks.metadata import authors_to_string
        from calibre.utils.date import isoformat
        ans = [(_('Title'), unicode(self.title))]
        ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))]
        ans += [(_('Publisher'), unicode(self.publisher))]
        ans += [(_('Producer'), unicode(self.book_producer))]
        ans += [(_('Comments'), unicode(self.comments))]
        ans += [('ISBN', unicode(self.isbn))]
        ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
        if self.series:
            ans += [(_('Series'), unicode(self.series) + ' #%s'%self.format_series_index())]
        ans += [(_('Languages'), u', '.join(self.languages))]
        if self.timestamp is not None:
            ans += [(_('Timestamp'), unicode(isoformat(self.timestamp, as_utc=False, sep=' ')))]
        if self.pubdate is not None:
            ans += [(_('Published'), unicode(isoformat(self.pubdate, as_utc=False, sep=' ')))]
        if self.rights is not None:
            ans += [(_('Rights'), unicode(self.rights))]
        for key in self.custom_field_keys():
            val = self.get(key, None)
            if val:
                (name, val) = self.format_field(key)
                ans += [(name, val)]
        for i, x in enumerate(ans):
            ans[i] = u'<tr><td><b>%s</b></td><td>%s</td></tr>'%x
        return u'<table>%s</table>'%u'\n'.join(ans)

    def __str__(self):
        return self.__unicode__().encode('utf-8')

    def __nonzero__(self):
        return bool(self.title or self.author or self.comments or self.tags)
Beispiel #16
0
    def _do_split(self,
                  db,
                  source_id,
                  misource,
                  splitepub,
                  origlines,
                  newspecs,
                  deftitle=None):

        linenums, changedtocs, checkedalways = newspecs
        # logger.debug("updated tocs:%s"%changedtocs)
        if not self.has_lines(linenums):
            return
        #logger.debug("2:%s"%(time.time()-self.t))
        self.t = time.time()

        #logger.debug("linenums:%s"%linenums)

        defauthors = None

        if not deftitle and prefs['copytoctitle']:
            if linenums[0] in changedtocs:
                deftitle = changedtocs[linenums[0]][0]  # already unicoded()'ed
            elif len(origlines[linenums[0]]['toc']) > 0:
                deftitle = unicode(origlines[linenums[0]]['toc'][0])
            #logger.debug("deftitle:%s"%deftitle)

        if not deftitle and prefs['copytitle']:
            deftitle = _("%s Split") % misource.title

        if prefs['copyauthors']:
            defauthors = misource.authors

        mi = MetaInformation(deftitle, defauthors)

        if prefs['copytags']:
            mi.tags = misource.tags  # [item for sublist in tagslists for item in sublist]

        if prefs['copylanguages']:
            mi.languages = misource.languages

        if prefs['copyseries']:
            mi.series = misource.series

        if prefs['copydate']:
            mi.timestamp = misource.timestamp

        if prefs['copyrating']:
            mi.rating = misource.rating

        if prefs['copypubdate']:
            mi.pubdate = misource.pubdate

        if prefs['copypublisher']:
            mi.publisher = misource.publisher

        if prefs['copyidentifiers']:
            mi.set_identifiers(misource.get_identifiers())

        if prefs['copycomments'] and misource.comments:
            mi.comments = "<p>" + _("Split from:") + "</p>" + misource.comments

        #logger.debug("mi:%s"%mi)
        book_id = db.create_book_entry(mi, add_duplicates=True)

        if prefs['copycover'] and misource.has_cover:
            db.set_cover(book_id, db.cover(source_id, index_is_id=True))

        #logger.debug("3:%s"%(time.time()-self.t))
        self.t = time.time()

        custom_columns = self.gui.library_view.model().custom_columns
        for col, action in six.iteritems(prefs['custom_cols']):
            #logger.debug("col: %s action: %s"%(col,action))

            if col not in custom_columns:
                #logger.debug("%s not an existing column, skipping."%col)
                continue

            coldef = custom_columns[col]
            #logger.debug("coldef:%s"%coldef)
            label = coldef['label']
            value = db.get_custom(source_id, label=label, index_is_id=True)
            if value:
                db.set_custom(book_id, value, label=label, commit=False)

        #logger.debug("3.5:%s"%(time.time()-self.t))
        self.t = time.time()

        if prefs['sourcecol'] != '' and prefs['sourcecol'] in custom_columns \
                and prefs['sourcetemplate']:
            val = SafeFormat().safe_format(prefs['sourcetemplate'], misource,
                                           'EpubSplit Source Template Error',
                                           misource)
            #logger.debug("Attempting to set %s to %s"%(prefs['sourcecol'],val))
            label = custom_columns[prefs['sourcecol']]['label']
            if custom_columns[prefs['sourcecol']]['datatype'] == 'series':
                val = val + (" [%s]" % self.book_count)
            db.set_custom(book_id, val, label=label, commit=False)
        self.book_count = self.book_count + 1
        db.commit()

        #logger.debug("4:%s"%(time.time()-self.t))
        self.t = time.time()

        self.gui.library_view.model().books_added(1)
        self.gui.library_view.select_rows([book_id])

        #logger.debug("5:%s"%(time.time()-self.t))
        self.t = time.time()

        editconfig_txt = _(
            'You can enable or disable Edit Metadata in Preferences > Plugins > EpubSplit.'
        )
        if prefs['editmetadata']:
            confirm(
                '\n' +
                _('''The book for the new Split EPUB has been created and default metadata filled in.

However, the EPUB will *not* be created until after you've reviewed, edited, and closed the metadata dialog that follows.

You can fill in the metadata yourself, or use download metadata for known books.

If you download or add a cover image, it will be included in the generated EPUB.'''
                  ) + '\n\n' + editconfig_txt + '\n',
                'epubsplit_created_now_edit_again', self.gui)
            self.gui.iactions['Edit Metadata'].edit_metadata(False)

        try:
            QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
            #logger.debug("5:%s"%(time.time()-self.t))
            self.t = time.time()
            self.gui.tags_view.recount()

            self.gui.status_bar.show_message(_('Splitting off from EPUB...'),
                                             60000)

            mi = db.get_metadata(book_id, index_is_id=True)

            outputepub = PersistentTemporaryFile(suffix='.epub')

            coverjpgpath = None
            if mi.has_cover:
                # grab the path to the real image.
                coverjpgpath = os.path.join(db.library_path,
                                            db.path(book_id, index_is_id=True),
                                            'cover.jpg')

            outlist = list(set(linenums + checkedalways))
            outlist.sort()
            splitepub.write_split_epub(outputepub,
                                       outlist,
                                       changedtocs=changedtocs,
                                       authoropts=mi.authors,
                                       titleopt=mi.title,
                                       descopt=mi.comments,
                                       tags=mi.tags,
                                       languages=mi.languages,
                                       coverjpgpath=coverjpgpath)

            #logger.debug("6:%s"%(time.time()-self.t))
            self.t = time.time()
            db.add_format_with_hooks(book_id,
                                     'EPUB',
                                     outputepub,
                                     index_is_id=True)

            #logger.debug("7:%s"%(time.time()-self.t))
            self.t = time.time()

            self.gui.status_bar.show_message(_('Finished splitting off EPUB.'),
                                             3000)
            self.gui.library_view.model().refresh_ids([book_id])
            self.gui.tags_view.recount()
            current = self.gui.library_view.currentIndex()
            self.gui.library_view.model().current_changed(
                current, self.previous)
            if self.gui.cover_flow:
                self.gui.cover_flow.dataChanged()
        finally:
            QApplication.restoreOverrideCursor()

        if not prefs['editmetadata']:
            confirm(
                '<p>' + '</p><p>'.join([
                    _('<b><u>%s</u> by %s</b> has been created and default metadata filled in.'
                      ) % (mi.title, ', '.join(mi.authors)),
                    _('EpubSplit now skips the Edit Metadata step by default.'
                      ), editconfig_txt
                ]) + '</p>', 'epubsplit_created_now_no_edit_again', self.gui)
Beispiel #17
0
 def get_value(self, orig_key, args, kwargs):
     ans = SafeFormat.get_value(self, orig_key, args, kwargs)
     return escape_formatting(ans)
Beispiel #18
0
class Metadata(object):
    '''
    A class representing all the metadata for a book. The various standard metadata
    fields are available as attributes of this object. You can also stick
    arbitrary attributes onto this object.

    Metadata from custom columns should be accessed via the get() method,
    passing in the lookup name for the column, for example: "#mytags".

    Use the :meth:`is_null` method to test if a field is null.

    This object also has functions to format fields into strings.

    The list of standard metadata fields grows with time is in
    :data:`STANDARD_METADATA_FIELDS`.

    Please keep the method based API of this class to a minimum. Every method
    becomes a reserved field name.
    '''
    __calibre_serializable__ = True

    def __init__(self,
                 title,
                 authors=(_('Unknown'), ),
                 other=None,
                 template_cache=None,
                 formatter=None):
        '''
        @param title: title or ``_('Unknown')``
        @param authors: List of strings or []
        @param other: None or a metadata object
        '''
        _data = copy.deepcopy(NULL_VALUES)
        _data.pop('language')
        object.__setattr__(self, '_data', _data)
        if other is not None:
            self.smart_update(other)
        else:
            if title:
                self.title = title
            if authors:
                # List of strings or []
                self.author = list(authors) if authors else [
                ]  # Needed for backward compatibility
                self.authors = list(authors) if authors else []
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        self.formatter = SafeFormat() if formatter is None else formatter
        self.template_cache = template_cache

    def is_null(self, field):
        '''
        Return True if the value of field is null in this object.
        'null' means it is unknown or evaluates to False. So a title of
        _('Unknown') is null or a language of 'und' is null.

        Be careful with numeric fields since this will return True for zero as
        well as None.

        Also returns True if the field does not exist.
        '''
        try:
            null_val = NULL_VALUES.get(field, None)
            val = getattr(self, field, None)
            return not val or val == null_val
        except:
            return True

    def __getattribute__(self, field):
        _data = object.__getattribute__(self, '_data')
        if field in SIMPLE_GET:
            return _data.get(field, None)
        if field in TOP_LEVEL_IDENTIFIERS:
            return _data.get('identifiers').get(field, None)
        if field == 'language':
            try:
                return _data.get('languages', [])[0]
            except:
                return NULL_VALUES['language']
        try:
            return object.__getattribute__(self, field)
        except AttributeError:
            pass
        if field in iter(_data['user_metadata'].keys()):
            d = _data['user_metadata'][field]
            val = d['#value#']
            if d['datatype'] != 'composite':
                return val
            if val is None:
                d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field
                val = d['#value#'] = self.formatter.safe_format(
                    d['display']['composite_template'],
                    self,
                    _('TEMPLATE ERROR'),
                    self,
                    column_name=field,
                    template_cache=self.template_cache).strip()
            return val
        if field.startswith('#') and field.endswith('_index'):
            try:
                return self.get_extra(field[:-6])
            except:
                pass
        raise AttributeError('Metadata object has no attribute named: ' +
                             repr(field))

    def __setattr__(self, field, val, extra=None):
        _data = object.__getattribute__(self, '_data')
        if field in SIMPLE_SET:
            if val is None:
                val = copy.copy(NULL_VALUES.get(field, None))
            _data[field] = val
        elif field in TOP_LEVEL_IDENTIFIERS:
            field, val = self._clean_identifier(field, val)
            identifiers = _data['identifiers']
            identifiers.pop(field, None)
            if val:
                identifiers[field] = val
        elif field == 'identifiers':
            if not val:
                val = copy.copy(NULL_VALUES.get('identifiers', None))
            self.set_identifiers(val)
        elif field == 'language':
            langs = []
            if val and val.lower() != 'und':
                langs = [val]
            _data['languages'] = langs
        elif field in iter(_data['user_metadata'].keys()):
            _data['user_metadata'][field]['#value#'] = val
            _data['user_metadata'][field]['#extra#'] = extra
        else:
            # You are allowed to stick arbitrary attributes onto this object as
            # long as they don't conflict with global or user metadata names
            # Don't abuse this privilege
            self.__dict__[field] = val

    def __iter__(self):
        return iter(object.__getattribute__(self, '_data').keys())

    def has_key(self, key):
        return key in object.__getattribute__(self, '_data')

    def deepcopy(self, class_generator=lambda: Metadata(None)):
        ''' Do not use this method unless you know what you are doing, if you
        want to create a simple clone of this object, use :meth:`deepcopy_metadata`
        instead. Class_generator must be a function that returns an instance
        of Metadata or a subclass of it.'''
        m = class_generator()
        if not isinstance(m, Metadata):
            return None
        object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__))
        return m

    def deepcopy_metadata(self):
        m = Metadata(None)
        object.__setattr__(
            m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
        return m

    def get(self, field, default=None):
        try:
            return self.__getattribute__(field)
        except AttributeError:
            return default

    def get_extra(self, field, default=None):
        _data = object.__getattribute__(self, '_data')
        if field in iter(_data['user_metadata'].keys()):
            try:
                return _data['user_metadata'][field]['#extra#']
            except:
                return default
        raise AttributeError('Metadata object has no attribute named: ' +
                             repr(field))

    def set(self, field, val, extra=None):
        self.__setattr__(field, val, extra)

    def get_identifiers(self):
        '''
        Return a copy of the identifiers dictionary.
        The dict is small, and the penalty for using a reference where a copy is
        needed is large. Also, we don't want any manipulations of the returned
        dict to show up in the book.
        '''
        ans = object.__getattribute__(self, '_data')['identifiers']
        if not ans:
            ans = {}
        return copy.deepcopy(ans)

    def _clean_identifier(self, typ, val):
        if typ:
            typ = ck(typ)
        if val:
            val = cv(val)
        return typ, val

    def set_identifiers(self, identifiers):
        '''
        Set all identifiers. Note that if you previously set ISBN, calling
        this method will delete it.
        '''
        cleaned = {ck(k): cv(v) for k, v in identifiers.items() if k and v}
        object.__getattribute__(self, '_data')['identifiers'] = cleaned

    def set_identifier(self, typ, val):
        'If val is empty, deletes identifier of type typ'
        typ, val = self._clean_identifier(typ, val)
        if not typ:
            return
        identifiers = object.__getattribute__(self, '_data')['identifiers']

        identifiers.pop(typ, None)
        if val:
            identifiers[typ] = val

    def has_identifier(self, typ):
        identifiers = object.__getattribute__(self, '_data')['identifiers']
        return typ in identifiers

    # field-oriented interface. Intended to be the same as in LibraryDatabase

    def standard_field_keys(self):
        '''
        return a list of all possible keys, even if this book doesn't have them
        '''
        return STANDARD_METADATA_FIELDS

    def custom_field_keys(self):
        '''
        return a list of the custom fields in this book
        '''
        return iter(
            object.__getattribute__(self, '_data')['user_metadata'].keys())

    def all_field_keys(self):
        '''
        All field keys known by this instance, even if their value is None
        '''
        _data = object.__getattribute__(self, '_data')
        return frozenset(
            ALL_METADATA_FIELDS.union(iter(_data['user_metadata'].keys())))

    def metadata_for_field(self, key):
        '''
        return metadata describing a standard or custom field.
        '''
        if key not in self.custom_field_keys():
            return self.get_standard_metadata(key, make_copy=False)
        return self.get_user_metadata(key, make_copy=False)

    def all_non_none_fields(self):
        '''
        Return a dictionary containing all non-None metadata fields, including
        the custom ones.
        '''
        result = {}
        _data = object.__getattribute__(self, '_data')
        for attr in STANDARD_METADATA_FIELDS:
            v = _data.get(attr, None)
            if v is not None:
                result[attr] = v
        # separate these because it uses the self.get(), not _data.get()
        for attr in TOP_LEVEL_IDENTIFIERS:
            v = self.get(attr, None)
            if v is not None:
                result[attr] = v
        for attr in _data['user_metadata'].keys():
            v = self.get(attr, None)
            if v is not None:
                result[attr] = v
                if _data['user_metadata'][attr]['datatype'] == 'series':
                    result[attr +
                           '_index'] = _data['user_metadata'][attr]['#extra#']
        return result

    # End of field-oriented interface

    # Extended interfaces. These permit one to get copies of metadata dictionaries, and to
    # get and set custom field metadata

    def get_standard_metadata(self, field, make_copy):
        '''
        return field metadata from the field if it is there. Otherwise return
        None. field is the key name, not the label. Return a copy if requested,
        just in case the user wants to change values in the dict.
        '''
        if field in field_metadata and field_metadata[field]['kind'] == 'field':
            if make_copy:
                return copy.deepcopy(field_metadata[field])
            return field_metadata[field]
        return None

    def get_all_standard_metadata(self, make_copy):
        '''
        return a dict containing all the standard field metadata associated with
        the book.
        '''
        if not make_copy:
            return field_metadata
        res = {}
        for k in field_metadata:
            if field_metadata[k]['kind'] == 'field':
                res[k] = copy.deepcopy(field_metadata[k])
        return res

    def get_all_user_metadata(self, make_copy):
        '''
        return a dict containing all the custom field metadata associated with
        the book.
        '''
        _data = object.__getattribute__(self, '_data')
        user_metadata = _data['user_metadata']
        if not make_copy:
            return user_metadata
        res = {}
        for k in user_metadata:
            res[k] = copy.deepcopy(user_metadata[k])
        return res

    def get_user_metadata(self, field, make_copy):
        '''
        return field metadata from the object if it is there. Otherwise return
        None. field is the key name, not the label. Return a copy if requested,
        just in case the user wants to change values in the dict.
        '''
        _data = object.__getattribute__(self, '_data')
        _data = _data['user_metadata']
        if field in _data:
            if make_copy:
                return copy.deepcopy(_data[field])
            return _data[field]
        return None

    def set_all_user_metadata(self, metadata):
        '''
        store custom field metadata into the object. Field is the key name
        not the label
        '''
        if metadata is None:
            traceback.print_stack()
            return

        um = {}
        for key, meta in metadata.items():
            m = meta.copy()
            if '#value#' not in m:
                if m['datatype'] == 'text' and m['is_multiple']:
                    m['#value#'] = []
                else:
                    m['#value#'] = None
            um[key] = m
        _data = object.__getattribute__(self, '_data')
        _data['user_metadata'] = um

    def set_user_metadata(self, field, metadata):
        '''
        store custom field metadata for one column into the object. Field is
        the key name not the label
        '''
        if field is not None:
            if not field.startswith('#'):
                raise AttributeError(
                    'Custom field name %s must begin with \'#\'' % repr(field))
            if metadata is None:
                traceback.print_stack()
                return
            m = dict(metadata)
            # Copying the elements should not be necessary. The objects referenced
            # in the dict should not change. Of course, they can be replaced.
            # for k,v in metadata.iteritems():
            #     m[k] = copy.copy(v)
            if '#value#' not in m:
                if m['datatype'] == 'text' and m['is_multiple']:
                    m['#value#'] = []
                else:
                    m['#value#'] = None
            _data = object.__getattribute__(self, '_data')
            _data['user_metadata'][field] = m

    def template_to_attribute(self, other, ops):
        '''
        Takes a list [(src,dest), (src,dest)], evaluates the template in the
        context of other, then copies the result to self[dest]. This is on a
        best-efforts basis. Some assignments can make no sense.
        '''
        if not ops:
            return
        from calibre.ebooks.metadata.book.formatter import SafeFormat
        formatter = SafeFormat()
        for op in ops:
            try:
                src = op[0]
                dest = op[1]
                val = formatter.safe_format(src, other,
                                            'PLUGBOARD TEMPLATE ERROR', other)
                if dest == 'tags':
                    self.set(dest,
                             [f.strip() for f in val.split(',') if f.strip()])
                elif dest == 'authors':
                    self.set(dest,
                             [f.strip() for f in val.split('&') if f.strip()])
                else:
                    self.set(dest, val)
            except:
                if DEBUG:
                    traceback.print_exc()

    # Old Metadata API {{{
    def print_all_attributes(self):
        for x in STANDARD_METADATA_FIELDS:
            prints('%s:' % x, getattr(self, x, 'None'))
        for x in self.custom_field_keys():
            meta = self.get_user_metadata(x, make_copy=False)
            if meta is not None:
                prints(x, meta)
        prints('--------------')

    def smart_update(self, other, replace_metadata=False):
        '''
        Merge the information in `other` into self. In case of conflicts, the information
        in `other` takes precedence, unless the information in `other` is NULL.
        '''
        def copy_not_none(dest, src, attr):
            v = getattr(src, attr, None)
            if v not in (None, NULL_VALUES.get(attr, None)):
                setattr(dest, attr, copy.deepcopy(v))

        unknown = _('Unknown')
        if other.title and other.title != unknown:
            self.title = other.title
            if hasattr(other, 'title_sort'):
                self.title_sort = other.title_sort

        if other.authors and (
                other.authors[0] != unknown or
            (not self.authors or
             (len(self.authors) == 1 and self.authors[0] == unknown
              and getattr(self, 'author_sort', None) == unknown))):
            self.authors = list(other.authors)
            if hasattr(other, 'author_sort_map'):
                self.author_sort_map = dict(other.author_sort_map)
            if hasattr(other, 'author_sort'):
                self.author_sort = other.author_sort

        if replace_metadata:
            # SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
            for attr in SC_COPYABLE_FIELDS:
                setattr(
                    self, attr,
                    getattr(other, attr,
                            1.0 if attr == 'series_index' else None))
            self.tags = other.tags
            self.cover_data = getattr(other, 'cover_data',
                                      NULL_VALUES['cover_data'])
            self.set_all_user_metadata(
                other.get_all_user_metadata(make_copy=True))
            for x in SC_FIELDS_COPY_NOT_NULL:
                copy_not_none(self, other, x)
            if callable(getattr(other, 'get_identifiers', None)):
                self.set_identifiers(other.get_identifiers())
            # language is handled below
        else:
            for attr in SC_COPYABLE_FIELDS:
                copy_not_none(self, other, attr)
            for x in SC_FIELDS_COPY_NOT_NULL:
                copy_not_none(self, other, x)

            if other.tags:
                # Case-insensitive but case preserving merging
                lotags = [t.lower() for t in other.tags]
                lstags = [t.lower() for t in self.tags]
                ot, st = list(map(frozenset, (lotags, lstags)))
                for t in st.intersection(ot):
                    sidx = lstags.index(t)
                    oidx = lotags.index(t)
                    self.tags[sidx] = other.tags[oidx]
                self.tags += [t for t in other.tags if t.lower() in ot - st]

            if getattr(other, 'cover_data', False):
                other_cover = other.cover_data[-1]
                self_cover = self.cover_data[-1] if self.cover_data else ''
                if not self_cover:
                    self_cover = ''
                if not other_cover:
                    other_cover = ''
                if len(other_cover) > len(self_cover):
                    self.cover_data = other.cover_data

            if callable(getattr(other, 'custom_field_keys', None)):
                for x in other.custom_field_keys():
                    meta = other.get_user_metadata(x, make_copy=True)
                    if meta is not None:
                        self_tags = self.get(x, [])
                        self.set_user_metadata(x,
                                               meta)  # get... did the deepcopy
                        other_tags = other.get(x, [])
                        if meta['datatype'] == 'text' and meta['is_multiple']:
                            # Case-insensitive but case preserving merging
                            lotags = [t.lower() for t in other_tags]
                            try:
                                lstags = [t.lower() for t in self_tags]
                            except TypeError:
                                # Happens if x is not a text, is_multiple field
                                # on self
                                lstags = []
                                self_tags = []
                            ot, st = list(map(frozenset, (lotags, lstags)))
                            for t in st.intersection(ot):
                                sidx = lstags.index(t)
                                oidx = lotags.index(t)
                                self_tags[sidx] = other_tags[oidx]
                            self_tags += [
                                t for t in other_tags if t.lower() in ot - st
                            ]
                            setattr(self, x, self_tags)

            my_comments = getattr(self, 'comments', '')
            other_comments = getattr(other, 'comments', '')
            if not my_comments:
                my_comments = ''
            if not other_comments:
                other_comments = ''
            if len(other_comments.strip()) > len(my_comments.strip()):
                self.comments = other_comments

            # Copy all the non-none identifiers
            if callable(getattr(other, 'get_identifiers', None)):
                d = self.get_identifiers()
                s = other.get_identifiers()
                d.update([v for v in s.items() if v[1] is not None])
                self.set_identifiers(d)
            else:
                # other structure not Metadata. Copy the top-level identifiers
                for attr in TOP_LEVEL_IDENTIFIERS:
                    copy_not_none(self, other, attr)

        other_lang = getattr(other, 'languages', [])
        if other_lang and other_lang != ['und']:
            self.languages = list(other_lang)
        if not getattr(self, 'series', None):
            self.series_index = None

    def format_series_index(self, val=None):
        from calibre.ebooks.metadata import fmt_sidx
        v = self.series_index if val is None else val
        try:
            x = float(v)
        except (ValueError, TypeError):
            x = 1
        return fmt_sidx(x)

    def authors_from_string(self, raw):
        from calibre.ebooks.metadata import string_to_authors
        self.authors = string_to_authors(raw)

    def format_authors(self):
        from calibre.ebooks.metadata import authors_to_string
        return authors_to_string(self.authors)

    def format_tags(self):
        return ', '.join([str(t) for t in sorted(self.tags, key=sort_key)])

    def format_rating(self, v=None, divide_by=1.0):
        if v is None:
            if self.rating is not None:
                return str(self.rating / divide_by)
            return 'None'
        return str(v / divide_by)

    def format_field(self, key, series_with_index=True):
        '''
        Returns the tuple (display_name, formatted_value)
        '''
        name, val, ign, ign = self.format_field_extended(
            key, series_with_index)
        return (name, val)

    def format_field_extended(self, key, series_with_index=True):
        from calibre.ebooks.metadata import authors_to_string
        '''
        returns the tuple (display_name, formatted_value, original_value,
        field_metadata)
        '''
        from calibre.utils.date import format_date

        # Handle custom series index
        if key.startswith('#') and key.endswith('_index'):
            tkey = key[:-6]  # strip the _index
            cmeta = self.get_user_metadata(tkey, make_copy=False)
            if cmeta and cmeta['datatype'] == 'series':
                if self.get(tkey):
                    res = self.get_extra(tkey)
                    return (str(cmeta['name'] + '_index'),
                            self.format_series_index(res), res, cmeta)
                else:
                    return (str(cmeta['name'] + '_index'), '', '', cmeta)

        if key in self.custom_field_keys():
            res = self.get(key, None)  # get evaluates all necessary composites
            cmeta = self.get_user_metadata(key, make_copy=False)
            name = str(cmeta['name'])
            if res is None or res == '':  # can't check "not res" because of numeric fields
                return (name, res, None, None)
            orig_res = res
            datatype = cmeta['datatype']
            if datatype == 'text' and cmeta['is_multiple']:
                res = cmeta['is_multiple']['list_to_ui'].join(res)
            elif datatype == 'series' and series_with_index:
                if self.get_extra(key) is not None:
                    res = res + \
                        ' [%s]'%self.format_series_index(val=self.get_extra(key))
            elif datatype == 'datetime':
                res = format_date(
                    res, cmeta['display'].get('date_format', 'dd MMM yyyy'))
            elif datatype == 'bool':
                res = _('Yes') if res else _('No')
            elif datatype == 'rating':
                res = '%.2g' % (res / 2.0)
            elif datatype in ['int', 'float']:
                try:
                    fmt = cmeta['display'].get('number_format', None)
                    res = fmt.format(res)
                except:
                    pass
            return (name, str(res), orig_res, cmeta)

        # convert top-level ids into their value
        if key in TOP_LEVEL_IDENTIFIERS:
            fmeta = field_metadata['identifiers']
            name = key
            res = self.get(key, None)
            return (name, res, res, fmeta)

        # Translate aliases into the standard field name
        fmkey = field_metadata.search_term_to_field_key(key)
        if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
            res = self.get(key, None)
            fmeta = field_metadata[fmkey]
            name = str(fmeta['name'])
            if res is None or res == '':
                return (name, res, None, None)
            orig_res = res
            name = str(fmeta['name'])
            datatype = fmeta['datatype']
            if key == 'authors':
                res = authors_to_string(res)
            elif key == 'series_index':
                res = self.format_series_index(res)
            elif datatype == 'text' and fmeta['is_multiple']:
                if isinstance(res, dict):
                    res = [k + ':' + v for k, v in list(res.items())]
                res = fmeta['is_multiple']['list_to_ui'].join(
                    sorted([_f for _f in res if _f], key=sort_key))
            elif datatype == 'series' and series_with_index:
                res = res + ' [%s]' % self.format_series_index()
            elif datatype == 'datetime':
                res = format_date(
                    res, fmeta['display'].get('date_format', 'dd MMM yyyy'))
            elif datatype == 'rating':
                res = '%.2g' % (res / 2.0)
            elif key == 'size':
                res = human_readable(res)
            return (name, str(res), orig_res, fmeta)

        return (None, None, None, None)

    def __unicode__(self):
        '''
        A string representation of this object, suitable for printing to
        console
        '''
        from calibre.utils.date import isoformat
        from calibre.ebooks.metadata import authors_to_string
        ans = []

        def fmt(x, y):
            ans.append('%-20s: %s' % (str(x), str(y)))

        fmt('Title', self.title)
        if self.title_sort:
            fmt('Title sort', self.title_sort)
        if self.authors:
            fmt(
                'Author(s)',
                authors_to_string(self.authors) +
                ((' [' + self.author_sort + ']') if self.author_sort
                 and self.author_sort != _('Unknown') else ''))
        if self.publisher:
            fmt('Publisher', self.publisher)
        if getattr(self, 'book_producer', False):
            fmt('Book Producer', self.book_producer)
        if self.tags:
            fmt('Tags', ', '.join([str(t) for t in self.tags]))
        if self.series:
            fmt('Series', self.series + ' #%s' % self.format_series_index())
        if not self.is_null('languages'):
            fmt('Languages', ', '.join(self.languages))
        if self.rating is not None:
            fmt('Rating',
                ('%.2g' % (float(self.rating) / 2.0)) if self.rating else '')
        if self.timestamp is not None:
            fmt('Timestamp', isoformat(self.timestamp))
        if self.pubdate is not None:
            fmt('Published', isoformat(self.pubdate))
        if self.rights is not None:
            fmt('Rights', str(self.rights))
        if self.identifiers:
            fmt(
                'Identifiers', ', '.join(
                    ['%s:%s' % (k, v) for k, v in self.identifiers.items()]))
        if self.comments:
            fmt('Comments', self.comments)

        for key in self.custom_field_keys():
            val = self.get(key, None)
            if val:
                (name, val) = self.format_field(key)
                fmt(name, str(val))
        return '\n'.join(ans)

    def to_html(self):
        '''
        A HTML representation of this object.
        '''
        from calibre.ebooks.metadata import authors_to_string
        from calibre.utils.date import isoformat
        ans = [(_('Title'), str(self.title))]
        ans += [(_('Author(s)'), (authors_to_string(self.authors)
                                  if self.authors else _('Unknown')))]
        ans += [(_('Publisher'), str(self.publisher))]
        ans += [(_('Producer'), str(self.book_producer))]
        ans += [(_('Comments'), str(self.comments))]
        ans += [('ISBN', str(self.isbn))]
        ans += [(_('Tags'), ', '.join([str(t) for t in self.tags]))]
        if self.series:
            ans += [(_('Series'),
                     str(self.series) + ' #%s' % self.format_series_index())]
        ans += [(_('Languages'), ', '.join(self.languages))]
        if self.timestamp is not None:
            ans += [(_('Timestamp'),
                     str(isoformat(self.timestamp, as_utc=False, sep=' ')))]
        if self.pubdate is not None:
            ans += [(_('Published'),
                     str(isoformat(self.pubdate, as_utc=False, sep=' ')))]
        if self.rights is not None:
            ans += [(_('Rights'), str(self.rights))]
        for key in self.custom_field_keys():
            val = self.get(key, None)
            if val:
                (name, val) = self.format_field(key)
                ans += [(name, val)]
        for i, x in enumerate(ans):
            ans[i] = '<tr><td><b>%s</b></td><td>%s</td></tr>' % x
        return '<table>%s</table>' % '\n'.join(ans)

    def __str__(self):
        return self.__unicode__().encode('utf-8')

    def __bool__(self):
        return bool(self.title or self.author or self.comments or self.tags)
Beispiel #19
0
    def get_collections(self,
                        collection_attributes,
                        collections_template=None,
                        template_globals=None):
        debug_print(
            "KTCollectionsBookList:get_collections - start - collection_attributes=",
            collection_attributes)

        collections = {}

        ca = []
        for c in collection_attributes:
            ca.append(c.lower())
        collection_attributes = ca
        debug_print(
            "KTCollectionsBookList:get_collections - collection_attributes=",
            collection_attributes)

        for book in self:
            tsval = book.get('title_sort', book.title)
            if tsval is None:
                tsval = book.title

            show_debug = self.is_debugging_title(tsval) or tsval is None
            if show_debug:  # or len(book.device_collections) > 0:
                debug_print('KTCollectionsBookList:get_collections - tsval=',
                            tsval, "book.title=", book.title,
                            "book.title_sort=", book.title_sort)
                debug_print(
                    'KTCollectionsBookList:get_collections - book.device_collections=',
                    book.device_collections)
                # debug_print(book)
            # Make sure we can identify this book via the lpath
            lpath = getattr(book, 'lpath', None)
            if lpath is None:
                continue
            # If the book is not in the current library, we don't want to use the metadata for the collections
            # or it is a book that cannot be put in a collection (such as recommendations or previews)
            if book.application_id is None or not book.can_put_on_shelves:
                # debug_print("KTCollectionsBookList:get_collections - Book not in current library or cannot be put in a collection")
                continue

            # Decide how we will build the collections. The default: leave the
            # book in all existing collections. Do not add any new ones.
            attrs = ['device_collections']
            if getattr(book, '_new_book', False):
                debug_print(
                    "KTCollectionsBookList:get_collections - sending new book")
                if prefs['manage_device_metadata'] == 'manual':
                    # Ensure that the book is in all the book's existing
                    # collections plus all metadata collections
                    attrs += collection_attributes
                else:
                    # For new books, both 'on_send' and 'on_connect' do the same
                    # thing. The book's existing collections are ignored. Put
                    # the book in collections defined by its metadata.
                    attrs = list(collection_attributes)
            elif prefs['manage_device_metadata'] == 'on_connect':
                # For existing books, modify the collections only if the user
                # specified 'on_connect'
                attrs = list(collection_attributes)
                for cat_name in self.device_managed_collections:
                    if cat_name in book.device_collections:
                        if cat_name not in collections:
                            collections[cat_name] = {}
                            if show_debug:
                                debug_print(
                                    "KTCollectionsBookList:get_collections - Device Managed Collection:",
                                    cat_name)
                        if lpath not in collections[cat_name]:
                            collections[cat_name][lpath] = book
                            if show_debug:
                                debug_print(
                                    "KTCollectionsBookList:get_collections - Device Managed Collection -added book to cat_name",
                                    cat_name)
                book.device_collections = []
            if show_debug:
                debug_print("KTCollectionsBookList:get_collections - attrs=",
                            attrs)

            if collections_template is not None:
                attrs.append('%template%')

            for attr in attrs:
                fm = None
                attr = attr.strip()
                if show_debug:
                    debug_print(
                        "KTCollectionsBookList:get_collections - attr='%s'" %
                        attr)

                # If attr is device_collections, then we cannot use
                # format_field, because we don't know the fields where the
                # values came from.
                if attr == 'device_collections':
                    doing_dc = True
                    val = book.device_collections  # is a list
                    if show_debug:
                        debug_print(
                            "KTCollectionsBookList:get_collections - adding book.device_collections",
                            book.device_collections)
                elif attr == '%template%':
                    doing_dc = False
                    val = ''
                    if collections_template is not None:
                        nv = SafeFormat().safe_format(
                            collections_template,
                            book,
                            'KOBO',
                            book,
                            global_vars=template_globals)
                        if show_debug:
                            debug_print(
                                "KTCollectionsBookList:get_collections collections_template - result",
                                nv)
                        if nv:
                            val = [
                                v.strip() for v in nv.split(':@:')
                                if v.strip()
                            ]
                else:
                    doing_dc = False
                    ign, val, orig_val, fm = book.format_field_extended(attr)
                    val = book.get(attr, None)
                    if show_debug:
                        debug_print(
                            "KTCollectionsBookList:get_collections - not device_collections"
                        )
                        debug_print('          ign=', ign, ', val=', val,
                                    ' orig_val=', orig_val, 'fm=', fm)
                        debug_print('          val=', val)

                if not val:
                    continue
                if isbytestring(val):
                    val = val.decode(preferred_encoding, 'replace')
                if isinstance(val, (list, tuple)):
                    val = list(val)
                    # debug_print("KTCollectionsBookList:get_collections - val is list=", val)
                elif fm is not None and fm['datatype'] == 'series':
                    val = [orig_val]
                elif fm is not None and fm['datatype'] == 'rating':
                    val = [str(orig_val / 2.0)]
                elif fm is not None and fm['datatype'] == 'text' and fm[
                        'is_multiple']:
                    if isinstance(orig_val, (list, tuple)):
                        val = orig_val
                    else:
                        val = [orig_val]
                    if show_debug:
                        debug_print(
                            "KTCollectionsBookList:get_collections - val is text and multiple",
                            val)
                elif fm is not None and fm['datatype'] == 'composite' and fm[
                        'is_multiple']:
                    if show_debug:
                        debug_print(
                            "KTCollectionsBookList:get_collections - val is compositeand multiple",
                            val)
                    val = [
                        v.strip()
                        for v in val.split(fm['is_multiple']['ui_to_list'])
                    ]
                else:
                    val = [val]
                if show_debug:
                    debug_print("KTCollectionsBookList:get_collections - val=",
                                val)

                for category in val:
                    # debug_print("KTCollectionsBookList:get_collections - category=", category)
                    if doing_dc:
                        pass  # No need to do anything with device_collections
                    elif fm is not None and fm[
                            'is_custom']:  # is a custom field
                        if fm['datatype'] == 'text' and len(category) > 1 and \
                                category[0] == '[' and category[-1] == ']':
                            continue
                    else:  # is a standard field
                        if attr == 'tags' and len(category) > 1 and \
                                category[0] == '[' and category[-1] == ']':
                            continue

                    # The category should not be None, but, it has happened.
                    if not category:
                        continue

                    cat_name = str(category).strip(' ,')

                    if cat_name not in collections:
                        collections[cat_name] = {}
                        if show_debug:
                            debug_print(
                                "KTCollectionsBookList:get_collections - created collection for cat_name",
                                cat_name)
                    if lpath not in collections[cat_name]:
                        collections[cat_name][lpath] = book
                        if show_debug:
                            debug_print(
                                "KTCollectionsBookList:get_collections - added book to collection for cat_name",
                                cat_name)
                    if show_debug:
                        debug_print(
                            "KTCollectionsBookList:get_collections - cat_name",
                            cat_name)

        # Sort collections
        result = {}

        for category, lpaths in collections.items():
            result[category] = lpaths.values()
        # debug_print("KTCollectionsBookList:get_collections - result=", result.keys())
        debug_print("KTCollectionsBookList:get_collections - end")
        return result