Exemplo n.º 1
0
class LibraryDatabase(object):
    ''' Emulate the old LibraryDatabase2 interface '''

    PATH_LIMIT = DB.PATH_LIMIT
    WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT
    CATEGORY_SORTS = CATEGORY_SORTS
    MATCH_TYPE = ('any', 'all')
    CUSTOM_DATA_TYPES = frozenset([
        'rating', 'text', 'comments', 'datetime', 'int', 'float', 'bool',
        'series', 'composite', 'enumeration'
    ])

    @classmethod
    def exists_at(cls, path):
        return path and os.path.exists(os.path.join(path, 'metadata.db'))

    def __init__(self,
                 library_path,
                 default_prefs=None,
                 read_only=False,
                 is_second_db=False,
                 progress_callback=lambda x, y: True,
                 restore_all_prefs=False):

        self.is_second_db = is_second_db
        self.listeners = set()

        backend = self.backend = create_backend(
            library_path,
            default_prefs=default_prefs,
            read_only=read_only,
            restore_all_prefs=restore_all_prefs,
            progress_callback=progress_callback,
            load_user_formatter_functions=not is_second_db)
        cache = self.new_api = Cache(backend)
        cache.init()
        self.data = View(cache)
        self.id = self.data.index_to_id
        self.row = self.data.id_to_index
        for x in ('get_property', 'count', 'refresh_ids', 'set_marked_ids',
                  'multisort', 'search', 'search_getting_ids'):
            setattr(self, x, getattr(self.data, x))

        self.is_case_sensitive = getattr(backend, 'is_case_sensitive', False)
        self.custom_field_name = backend.custom_field_name

        self.last_update_check = self.last_modified()

        if not self.is_second_db:
            set_saved_searches(self, 'saved_searches')

    def close(self):
        if hasattr(self, 'new_api'):
            self.new_api.close()

    def break_cycles(self):
        if hasattr(self, 'backend'):
            delattr(self.backend, 'field_metadata')
            self.data.cache.backend = None
            self.data.cache = None
            for x in (
                    'data',
                    'backend',
                    'new_api',
                    'listeners',
            ):
                delattr(self, x)

    # Library wide properties {{{
    @property
    def prefs(self):
        return self.new_api.backend.prefs

    @property
    def field_metadata(self):
        return self.backend.field_metadata

    @property
    def user_version(self):
        return self.backend.user_version

    @property
    def library_id(self):
        return self.backend.library_id

    @property
    def library_path(self):
        return self.backend.library_path

    @property
    def dbpath(self):
        return self.backend.dbpath

    def last_modified(self):
        return self.new_api.last_modified()

    def check_if_modified(self):
        if self.last_modified() > self.last_update_check:
            self.backend.reopen()
            self.new_api.reload_from_db()
            self.data.refresh(
                clear_caches=False
            )  # caches are already cleared by reload_from_db()
        self.last_update_check = utcnow()

    @property
    def custom_column_num_map(self):
        return self.backend.custom_column_num_map

    @property
    def custom_column_label_map(self):
        return self.backend.custom_column_label_map

    @property
    def FIELD_MAP(self):
        return self.backend.FIELD_MAP

    @property
    def formatter_template_cache(self):
        return self.data.cache.formatter_template_cache

    def initialize_template_cache(self):
        self.data.cache.initialize_template_cache()

    def all_ids(self):
        'All book ids in the db. This can no longer be a generator because of db locking.'
        return tuple(self.new_api.all_book_ids())

    def is_empty(self):
        with self.new_api.safe_read_lock:
            return not bool(self.new_api.fields['title'].table.book_col_map)

    def get_usage_count_by_id(self, field):
        return [[k, v]
                for k, v in self.new_api.get_usage_count_by_id(field).items()]

    def field_id_map(self, field):
        return [(k, v) for k, v in self.new_api.get_id_map(field).items()]

    def get_custom_items_with_ids(self, label=None, num=None):
        try:
            return [[k, v] for k, v in self.new_api.get_id_map(
                self.custom_field_name(label, num)).items()]
        except ValueError:
            return []

    def refresh(self, field=None, ascending=True):
        self.data.refresh(field=field, ascending=ascending)

    def get_id_from_uuid(self, uuid):
        if uuid:
            return self.new_api.lookup_by_uuid(uuid)

    def add_listener(self, listener):
        '''
        Add a listener. Will be called on change events with two arguments.
        Event name and list of affected ids.
        '''
        self.listeners.add(listener)

    def notify(self, event, ids=[]):
        'Notify all listeners'
        for listener in self.listeners:
            try:
                listener(event, ids)
            except:
                traceback.print_exc()
                continue

    # }}}

    def path(self, index, index_is_id=False):
        'Return the relative path to the directory containing this books files as a unicode string.'
        book_id = index if index_is_id else self.id(index)
        return self.new_api.field_for('path', book_id).replace('/', os.sep)

    def abspath(self, index, index_is_id=False, create_dirs=True):
        'Return the absolute path to the directory containing this books files as a unicode string.'
        path = os.path.join(self.library_path,
                            self.path(index, index_is_id=index_is_id))
        if create_dirs and not os.path.exists(path):
            os.makedirs(path)
        return path

    # Adding books {{{
    def create_book_entry(self,
                          mi,
                          cover=None,
                          add_duplicates=True,
                          force_id=None):
        ret = self.new_api.create_book_entry(mi,
                                             cover=cover,
                                             add_duplicates=add_duplicates,
                                             force_id=force_id)
        if ret is not None:
            self.data.books_added((ret, ))
        return ret

    def add_books(self,
                  paths,
                  formats,
                  metadata,
                  add_duplicates=True,
                  return_ids=False):
        books = [(mi, {
            fmt: path
        }) for mi, path, fmt in zip(metadata, paths, formats)]
        book_ids, duplicates = self.new_api.add_books(
            books, add_duplicates=add_duplicates, dbapi=self)
        if duplicates:
            paths, formats, metadata = [], [], []
            for mi, format_map in duplicates:
                metadata.append(mi)
                for fmt, path in format_map.items():
                    formats.append(fmt)
                    paths.append(path)
            duplicates = (paths, formats, metadata)
        ids = book_ids if return_ids else len(book_ids)
        if book_ids:
            self.data.books_added(book_ids)
        return duplicates or None, ids

    def import_book(self,
                    mi,
                    formats,
                    notify=True,
                    import_hooks=True,
                    apply_import_tags=True,
                    preserve_uuid=False):
        format_map = {}
        for path in formats:
            ext = os.path.splitext(path)[1][1:].upper()
            if ext == 'OPF':
                continue
            format_map[ext] = path
        book_ids, duplicates = self.new_api.add_books(
            [(mi, format_map)],
            add_duplicates=True,
            apply_import_tags=apply_import_tags,
            preserve_uuid=preserve_uuid,
            dbapi=self,
            run_hooks=import_hooks)
        if book_ids:
            self.data.books_added(book_ids)
        if notify:
            self.notify('add', book_ids)
        return book_ids[0]

    def find_books_in_directory(self,
                                dirpath,
                                single_book_per_directory,
                                compiled_rules=()):
        return find_books_in_directory(dirpath,
                                       single_book_per_directory,
                                       compiled_rules=compiled_rules)

    def import_book_directory_multiple(self,
                                       dirpath,
                                       callback=None,
                                       added_ids=None,
                                       compiled_rules=()):
        return import_book_directory_multiple(self,
                                              dirpath,
                                              callback=callback,
                                              added_ids=added_ids,
                                              compiled_rules=compiled_rules)

    def import_book_directory(self,
                              dirpath,
                              callback=None,
                              added_ids=None,
                              compiled_rules=()):
        return import_book_directory(self,
                                     dirpath,
                                     callback=callback,
                                     added_ids=added_ids,
                                     compiled_rules=compiled_rules)

    def recursive_import(self,
                         root,
                         single_book_per_directory=True,
                         callback=None,
                         added_ids=None,
                         compiled_rules=()):
        return recursive_import(
            self,
            root,
            single_book_per_directory=single_book_per_directory,
            callback=callback,
            added_ids=added_ids,
            compiled_rules=compiled_rules)

    def add_catalog(self, path, title):
        book_id, new_book_added = add_catalog(self.new_api,
                                              path,
                                              title,
                                              dbapi=self)
        if book_id is not None and new_book_added:
            self.data.books_added((book_id, ))
        return book_id

    def add_news(self, path, arg):
        book_id = add_news(self.new_api, path, arg, dbapi=self)
        if book_id is not None:
            self.data.books_added((book_id, ))
        return book_id

    def add_format(self,
                   index,
                   fmt,
                   stream,
                   index_is_id=False,
                   path=None,
                   notify=True,
                   replace=True,
                   copy_function=None):
        ''' path and copy_function are ignored by the new API '''
        book_id = index if index_is_id else self.id(index)
        ret = self.new_api.add_format(book_id,
                                      fmt,
                                      stream,
                                      replace=replace,
                                      run_hooks=False,
                                      dbapi=self)
        self.notify('metadata', [book_id])
        return ret

    def add_format_with_hooks(self,
                              index,
                              fmt,
                              fpath,
                              index_is_id=False,
                              path=None,
                              notify=True,
                              replace=True):
        ''' path is ignored by the new API '''
        book_id = index if index_is_id else self.id(index)
        ret = self.new_api.add_format(book_id,
                                      fmt,
                                      fpath,
                                      replace=replace,
                                      run_hooks=True,
                                      dbapi=self)
        self.notify('metadata', [book_id])
        return ret

    # }}}

    # Custom data {{{
    def add_custom_book_data(self, book_id, name, val):
        self.new_api.add_custom_book_data(name, {book_id: val})

    def add_multiple_custom_book_data(self, name, val_map, delete_first=False):
        self.new_api.add_custom_book_data(name,
                                          val_map,
                                          delete_first=delete_first)

    def get_custom_book_data(self, book_id, name, default=None):
        return self.new_api.get_custom_book_data(name,
                                                 book_ids={book_id},
                                                 default=default).get(
                                                     book_id, default)

    def get_all_custom_book_data(self, name, default=None):
        return self.new_api.get_custom_book_data(name, default=default)

    def delete_custom_book_data(self, book_id, name):
        self.new_api.delete_custom_book_data(name, book_ids=(book_id, ))

    def delete_all_custom_book_data(self, name):
        self.new_api.delete_custom_book_data(name)

    def get_ids_for_custom_book_data(self, name):
        return list(self.new_api.get_ids_for_custom_book_data(name))

    # }}}

    def sort(self, field, ascending, subsort=False):
        self.multisort([(field, ascending)])

    def get_field(self, index, key, default=None, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        mi = self.new_api.get_metadata(book_id, get_cover=key == 'cover')
        return mi.get(key, default)

    def cover_last_modified(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.cover_last_modified(
            book_id) or self.last_modified()

    def cover(self,
              index,
              index_is_id=False,
              as_file=False,
              as_image=False,
              as_path=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.cover(book_id,
                                  as_file=as_file,
                                  as_image=as_image,
                                  as_path=as_path)

    def copy_cover_to(self,
                      index,
                      dest,
                      index_is_id=False,
                      windows_atomic_move=None,
                      use_hardlink=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.copy_cover_to(book_id,
                                          dest,
                                          use_hardlink=use_hardlink)

    def copy_format_to(self,
                       index,
                       fmt,
                       dest,
                       index_is_id=False,
                       windows_atomic_move=None,
                       use_hardlink=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.copy_format_to(book_id,
                                           fmt,
                                           dest,
                                           use_hardlink=use_hardlink)

    def delete_book(self,
                    book_id,
                    notify=True,
                    commit=True,
                    permanent=False,
                    do_clean=True):
        self.new_api.remove_books((book_id, ), permanent=permanent)
        self.data.books_deleted((book_id, ))
        if notify:
            self.notify('delete', [book_id])

    def dirtied(self, book_ids, commit=True):
        self.new_api.mark_as_dirty(
            frozenset(book_ids) if book_ids is not None else book_ids)

    def dirty_queue_length(self):
        return self.new_api.dirty_queue_length()

    def dump_metadata(self,
                      book_ids=None,
                      remove_from_dirtied=True,
                      commit=True,
                      callback=None):
        self.new_api.dump_metadata(book_ids=book_ids,
                                   remove_from_dirtied=remove_from_dirtied,
                                   callback=callback)

    def authors_sort_strings(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return list(
            self.new_api.author_sort_strings_for_books((book_id, ))[book_id])

    def author_sort_from_book(self, index, index_is_id=False):
        return ' & '.join(
            self.authors_sort_strings(index, index_is_id=index_is_id))

    def authors_with_sort_strings(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        with self.new_api.safe_read_lock:
            authors = self.new_api._field_ids_for('authors', book_id)
            adata = self.new_api._author_data(authors)
            return [(aid, adata[aid]['name'], adata[aid]['sort'],
                     adata[aid]['link']) for aid in authors]

    def set_sort_field_for_author(self,
                                  old_id,
                                  new_sort,
                                  commit=True,
                                  notify=False):
        changed_books = self.new_api.set_sort_for_authors({old_id: new_sort})
        if notify:
            self.notify('metadata', list(changed_books))

    def set_link_field_for_author(self, aid, link, commit=True, notify=False):
        changed_books = self.new_api.set_link_for_authors({aid: link})
        if notify:
            self.notify('metadata', list(changed_books))

    def book_on_device(self, book_id):
        with self.new_api.safe_read_lock:
            return self.new_api.fields['ondevice'].book_on_device(book_id)

    def book_on_device_string(self, book_id):
        return self.new_api.field_for('ondevice', book_id)

    def set_book_on_device_func(self, func):
        self.new_api.fields['ondevice'].set_book_on_device_func(func)

    @property
    def book_on_device_func(self):
        return self.new_api.fields['ondevice'].book_on_device_func

    def books_in_series(self, series_id):
        with self.new_api.safe_read_lock:
            book_ids = self.new_api._books_for_field('series', series_id)
            ff = self.new_api._field_for
            return sorted(book_ids, key=lambda x: ff('series_index', x))

    def books_in_series_of(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        series_ids = self.new_api.field_ids_for('series', book_id)
        if not series_ids:
            return []
        return self.books_in_series(series_ids[0])

    def books_with_same_title(self, mi, all_matches=True):
        title = mi.title
        ans = set()
        if title:
            title = icu_lower(force_unicode(title))
            for book_id, x in self.new_api.get_id_map('title').items():
                if icu_lower(x) == title:
                    ans.add(book_id)
                    if not all_matches:
                        break
        return ans

    def set_conversion_options(self, book_id, fmt, options):
        self.new_api.set_conversion_options({book_id: options}, fmt=fmt)

    def conversion_options(self, book_id, fmt):
        return self.new_api.conversion_options(book_id, fmt=fmt)

    def has_conversion_options(self, ids, format='PIPE'):
        return self.new_api.has_conversion_options(ids, fmt=format)

    def delete_conversion_options(self, book_id, fmt, commit=True):
        self.new_api.delete_conversion_options((book_id, ), fmt=fmt)

    def set(self, index, field, val, allow_case_change=False):
        book_id = self.id(index)
        try:
            return self.new_api.set_field(field, {book_id: val},
                                          allow_case_change=allow_case_change)
        finally:
            self.notify('metadata', [book_id])

    def set_identifier(self, book_id, typ, val, notify=True, commit=True):
        with self.new_api.write_lock:
            identifiers = self.new_api._field_for('identifiers', book_id)
            typ, val = clean_identifier(typ, val)
            if typ:
                identifiers[typ] = val
                self.new_api._set_field('identifiers', {book_id: identifiers})
        self.notify('metadata', [book_id])

    def set_isbn(self, book_id, isbn, notify=True, commit=True):
        self.set_identifier(book_id,
                            'isbn',
                            isbn,
                            notify=notify,
                            commit=commit)

    def set_tags(self,
                 book_id,
                 tags,
                 append=False,
                 notify=True,
                 commit=True,
                 allow_case_change=False):
        tags = tags or []
        with self.new_api.write_lock:
            if append:
                otags = self.new_api._field_for('tags', book_id)
                existing = {icu_lower(x) for x in otags}
                tags = list(otags) + [
                    x for x in tags if icu_lower(x) not in existing
                ]
            ret = self.new_api._set_field('tags', {book_id: tags},
                                          allow_case_change=allow_case_change)
        if notify:
            self.notify('metadata', [book_id])
        return ret

    def set_metadata(self,
                     book_id,
                     mi,
                     ignore_errors=False,
                     set_title=True,
                     set_authors=True,
                     commit=True,
                     force_changes=False,
                     notify=True):
        self.new_api.set_metadata(book_id,
                                  mi,
                                  ignore_errors=ignore_errors,
                                  set_title=set_title,
                                  set_authors=set_authors,
                                  force_changes=force_changes)
        if notify:
            self.notify('metadata', [book_id])

    def remove_all_tags(self, ids, notify=False, commit=True):
        self.new_api.set_field('tags', {book_id: () for book_id in ids})
        if notify:
            self.notify('metadata', ids)

    def _do_bulk_modify(self, field, ids, add, remove, notify):
        add = cleanup_tags(add)
        remove = cleanup_tags(remove)
        remove = set(remove) - set(add)
        if not ids or (not add and not remove):
            return

        remove = {icu_lower(x) for x in remove}
        with self.new_api.write_lock:
            val_map = {}
            for book_id in ids:
                tags = list(self.new_api._field_for(field, book_id))
                existing = {icu_lower(x) for x in tags}
                tags.extend(t for t in add if icu_lower(t) not in existing)
                tags = tuple(t for t in tags if icu_lower(t) not in remove)
                val_map[book_id] = tags
            self.new_api._set_field(field, val_map, allow_case_change=False)

        if notify:
            self.notify('metadata', ids)

    def bulk_modify_tags(self, ids, add=[], remove=[], notify=False):
        self._do_bulk_modify('tags', ids, add, remove, notify)

    def set_custom_bulk_multiple(self,
                                 ids,
                                 add=[],
                                 remove=[],
                                 label=None,
                                 num=None,
                                 notify=False):
        data = self.backend.custom_field_metadata(label, num)
        if not data['editable']:
            raise ValueError('Column %r is not editable' % data['label'])
        if data['datatype'] != 'text' or not data['is_multiple']:
            raise ValueError('Column %r is not text/multiple' % data['label'])
        field = self.custom_field_name(label, num)
        self._do_bulk_modify(field, ids, add, remove, notify)

    def unapply_tags(self, book_id, tags, notify=True):
        self.bulk_modify_tags((book_id, ), remove=tags, notify=notify)

    def is_tag_used(self, tag):
        return icu_lower(tag) in {
            icu_lower(x)
            for x in self.new_api.all_field_names('tags')
        }

    def delete_tag(self, tag):
        self.delete_tags((tag, ))

    def delete_tags(self, tags):
        with self.new_api.write_lock:
            tag_map = {
                icu_lower(v): k
                for k, v in self.new_api._get_id_map('tags').items()
            }
            tag_ids = (tag_map.get(icu_lower(tag), None) for tag in tags)
            tag_ids = tuple(tid for tid in tag_ids if tid is not None)
            if tag_ids:
                self.new_api._remove_items('tags', tag_ids)

    def has_id(self, book_id):
        return self.new_api.has_id(book_id)

    def format(self,
               index,
               fmt,
               index_is_id=False,
               as_file=False,
               mode='r+b',
               as_path=False,
               preserve_filename=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format(book_id,
                                   fmt,
                                   as_file=as_file,
                                   as_path=as_path,
                                   preserve_filename=preserve_filename)

    def format_abspath(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format_abspath(book_id, fmt)

    def format_path(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.format_abspath(book_id, fmt)
        if ans is None:
            raise NoSuchFormat('Record %d has no format: %s' % (book_id, fmt))
        return ans

    def format_files(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return [(v, k) for k, v in self.new_api.format_files(book_id).items()]

    def format_metadata(self,
                        book_id,
                        fmt,
                        allow_cache=True,
                        update_db=False,
                        commit=False):
        return self.new_api.format_metadata(book_id,
                                            fmt,
                                            allow_cache=allow_cache,
                                            update_db=update_db)

    def format_last_modified(self, book_id, fmt):
        m = self.format_metadata(book_id, fmt)
        if m:
            return m['mtime']

    def formats(self, index, index_is_id=False, verify_formats=True):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.formats(book_id, verify_formats=verify_formats)
        if ans:
            return ','.join(ans)

    def has_format(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.has_format(book_id, fmt)

    def refresh_format_cache(self):
        self.new_api.refresh_format_cache()

    def refresh_ondevice(self):
        self.new_api.refresh_ondevice()

    def tags_older_than(self,
                        tag,
                        delta,
                        must_have_tag=None,
                        must_have_authors=None):
        for book_id in sorted(
                self.new_api.tags_older_than(
                    tag,
                    delta=delta,
                    must_have_tag=must_have_tag,
                    must_have_authors=must_have_authors)):
            yield book_id

    def sizeof_format(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format_metadata(book_id, fmt).get('size', None)

    def get_metadata(self,
                     index,
                     index_is_id=False,
                     get_cover=False,
                     get_user_categories=True,
                     cover_as_data=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.get_metadata(
            book_id,
            get_cover=get_cover,
            get_user_categories=get_user_categories,
            cover_as_data=cover_as_data)

    def rename_series(self, old_id, new_name, change_index=True):
        self.new_api.rename_items('series', {old_id: new_name},
                                  change_index=change_index)

    def get_custom(self, index, label=None, num=None, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.field_for(self.custom_field_name(label, num),
                                     book_id)
        if isinstance(ans, tuple):
            ans = list(ans)
        return ans

    def get_custom_extra(self, index, label=None, num=None, index_is_id=False):
        data = self.backend.custom_field_metadata(label, num)
        # add future datatypes with an extra column here
        if data['datatype'] != 'series':
            return None
        book_id = index if index_is_id else self.id(index)
        return self.new_api.field_for(
            self.custom_field_name(label, num) + '_index', book_id)

    def get_custom_and_extra(self,
                             index,
                             label=None,
                             num=None,
                             index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        data = self.backend.custom_field_metadata(label, num)
        ans = self.new_api.field_for(self.custom_field_name(label, num),
                                     book_id)
        if isinstance(ans, tuple):
            ans = list(ans)
        if data['datatype'] != 'series':
            return (ans, None)
        return (ans,
                self.new_api.field_for(
                    self.custom_field_name(label, num) + '_index', book_id))

    def get_next_cc_series_num_for(self, series, label=None, num=None):
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] != 'series':
            return None
        return self.new_api.get_next_series_num_for(
            series, field=self.custom_field_name(label, num))

    def is_item_used_in_multiple(self, item, label=None, num=None):
        existing_tags = self.all_custom(label=label, num=num)
        return icu_lower(item) in {icu_lower(t) for t in existing_tags}

    def delete_custom_item_using_id(self, item_id, label=None, num=None):
        self.new_api.remove_items(self.custom_field_name(label, num),
                                  (item_id, ))

    def rename_custom_item(self, old_id, new_name, label=None, num=None):
        self.new_api.rename_items(self.custom_field_name(label, num),
                                  {old_id: new_name},
                                  change_index=False)

    def delete_item_from_multiple(self, item, label=None, num=None):
        field = self.custom_field_name(label, num)
        existing = self.new_api.get_id_map(field)
        rmap = {icu_lower(v): k for k, v in existing.items()}
        item_id = rmap.get(icu_lower(item), None)
        if item_id is None:
            return []
        return list(self.new_api.remove_items(field, (item_id, )))

    def set_custom(self,
                   book_id,
                   val,
                   label=None,
                   num=None,
                   append=False,
                   notify=True,
                   extra=None,
                   commit=True,
                   allow_case_change=False):
        field = self.custom_field_name(label, num)
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] == 'composite':
            return set()
        if not data['editable']:
            raise ValueError('Column %r is not editable' % data['label'])
        if data['datatype'] == 'enumeration' and (
                val and val not in data['display']['enum_values']):
            return set()
        with self.new_api.write_lock:
            if append and data['is_multiple']:
                current = self.new_api._field_for(field, book_id)
                existing = {icu_lower(x) for x in current}
                val = current + tuple(
                    x for x in self.new_api.fields[field].writer.adapter(val)
                    if icu_lower(x) not in existing)
                affected_books = self.new_api._set_field(
                    field, {book_id: val}, allow_case_change=allow_case_change)
            else:
                affected_books = self.new_api._set_field(
                    field, {book_id: val}, allow_case_change=allow_case_change)
            if data['datatype'] == 'series':
                s, sidx = get_series_values(val)
                if sidx is None:
                    extra = 1.0 if extra is None else extra
                    self.new_api._set_field(field + '_index', {book_id: extra})
        if notify and affected_books:
            self.notify('metadata', list(affected_books))
        return affected_books

    def set_custom_bulk(self,
                        ids,
                        val,
                        label=None,
                        num=None,
                        append=False,
                        notify=True,
                        extras=None):
        if extras is not None and len(extras) != len(ids):
            raise ValueError('Length of ids and extras is not the same')
        field = self.custom_field_name(label, num)
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] == 'composite':
            return set()
        if data['datatype'] == 'enumeration' and (
                val and val not in data['display']['enum_values']):
            return
        if not data['editable']:
            raise ValueError('Column %r is not editable' % data['label'])

        if append:
            for book_id in ids:
                self.set_custom(book_id,
                                val,
                                label=label,
                                num=num,
                                append=True,
                                notify=False)
        else:
            with self.new_api.write_lock:
                self.new_api._set_field(field,
                                        {book_id: val
                                         for book_id in ids},
                                        allow_case_change=False)
            if extras is not None:
                self.new_api._set_field(
                    field + '_index',
                    {book_id: val
                     for book_id, val in zip(ids, extras)})
        if notify:
            self.notify('metadata', list(ids))

    def delete_custom_column(self, label=None, num=None):
        self.new_api.delete_custom_column(label, num)

    def create_custom_column(self,
                             label,
                             name,
                             datatype,
                             is_multiple,
                             editable=True,
                             display={}):
        return self.new_api.create_custom_column(label,
                                                 name,
                                                 datatype,
                                                 is_multiple,
                                                 editable=editable,
                                                 display=display)

    def set_custom_column_metadata(self,
                                   num,
                                   name=None,
                                   label=None,
                                   is_editable=None,
                                   display=None,
                                   notify=True,
                                   update_last_modified=False):
        changed = self.new_api.set_custom_column_metadata(
            num,
            name=name,
            label=label,
            is_editable=is_editable,
            display=display,
            update_last_modified=update_last_modified)
        if changed and notify:
            self.notify('metadata', [])

    def remove_cover(self, book_id, notify=True, commit=True):
        self.new_api.set_cover({book_id: None})
        if notify:
            self.notify('cover', [book_id])

    def set_cover(self, book_id, data, notify=True, commit=True):
        self.new_api.set_cover({book_id: data})
        if notify:
            self.notify('cover', [book_id])

    def original_fmt(self, book_id, fmt):
        nfmt = ('ORIGINAL_%s' % fmt).upper()
        return nfmt if self.new_api.has_format(book_id, nfmt) else fmt

    def save_original_format(self, book_id, fmt, notify=True):
        ret = self.new_api.save_original_format(book_id, fmt)
        if ret and notify:
            self.notify('metadata', [book_id])
        return ret

    def restore_original_format(self, book_id, original_fmt, notify=True):
        ret = self.new_api.restore_original_format(book_id, original_fmt)
        if ret and notify:
            self.notify('metadata', [book_id])
        return ret

    def remove_format(self,
                      index,
                      fmt,
                      index_is_id=False,
                      notify=True,
                      commit=True,
                      db_only=False):
        book_id = index if index_is_id else self.id(index)
        self.new_api.remove_formats({book_id: (fmt, )}, db_only=db_only)
        if notify:
            self.notify('metadata', [book_id])

    # Private interface {{{
    def __iter__(self):
        for row in self.data.iterall():
            yield row

    def _get_next_series_num_for_list(self, series_indices):
        return _get_next_series_num_for_list(series_indices)

    def _get_series_values(self, val):
        return _get_series_values(val)
Exemplo n.º 2
0
class LibraryDatabase(object):

    ''' Emulate the old LibraryDatabase2 interface '''

    PATH_LIMIT = DB.PATH_LIMIT
    WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT
    CATEGORY_SORTS = CATEGORY_SORTS
    MATCH_TYPE = ('any', 'all')
    CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
        'int', 'float', 'bool', 'series', 'composite', 'enumeration'])

    @classmethod
    def exists_at(cls, path):
        return path and os.path.exists(os.path.join(path, 'metadata.db'))

    def __init__(self, library_path,
            default_prefs=None, read_only=False, is_second_db=False,
            progress_callback=lambda x, y:True, restore_all_prefs=False):

        self.is_second_db = is_second_db
        self.listeners = set()

        backend = self.backend = create_backend(library_path, default_prefs=default_prefs,
                    read_only=read_only, restore_all_prefs=restore_all_prefs,
                    progress_callback=progress_callback)
        cache = self.new_api = Cache(backend)
        cache.init()
        self.data = View(cache)
        self.id = self.data.index_to_id
        self.row = self.data.id_to_index
        for x in ('get_property', 'count', 'refresh_ids', 'set_marked_ids',
                  'multisort', 'search', 'search_getting_ids'):
            setattr(self, x, getattr(self.data, x))

        self.is_case_sensitive = getattr(backend, 'is_case_sensitive', False)
        self.custom_field_name = backend.custom_field_name

        self.last_update_check = self.last_modified()

        if not self.is_second_db:
            set_saved_searches(self, 'saved_searches')

    def close(self):
        self.new_api.close()

    def break_cycles(self):
        delattr(self.backend, 'field_metadata')
        self.data.cache.backend = None
        self.data.cache = None
        for x in ('data', 'backend', 'new_api', 'listeners',):
            delattr(self, x)

    # Library wide properties {{{
    @property
    def prefs(self):
        return self.new_api.backend.prefs

    @property
    def field_metadata(self):
        return self.backend.field_metadata

    @property
    def user_version(self):
        return self.backend.user_version

    @property
    def library_id(self):
        return self.backend.library_id

    @property
    def library_path(self):
        return self.backend.library_path

    @property
    def dbpath(self):
        return self.backend.dbpath

    def last_modified(self):
        return self.new_api.last_modified()

    def check_if_modified(self):
        if self.last_modified() > self.last_update_check:
            self.backend.reopen()
            self.new_api.reload_from_db()
            self.data.refresh(clear_caches=False)  # caches are already cleared by reload_from_db()
        self.last_update_check = utcnow()

    @property
    def custom_column_num_map(self):
        return self.backend.custom_column_num_map

    @property
    def custom_column_label_map(self):
        return self.backend.custom_column_label_map

    @property
    def FIELD_MAP(self):
        return self.backend.FIELD_MAP

    @property
    def formatter_template_cache(self):
        return self.data.cache.formatter_template_cache

    def initialize_template_cache(self):
        self.data.cache.initialize_template_cache()

    def all_ids(self):
        'All book ids in the db. This can no longer be a generator because of db locking.'
        return tuple(self.new_api.all_book_ids())

    def is_empty(self):
        with self.new_api.safe_read_lock:
            return not bool(self.new_api.fields['title'].table.book_col_map)

    def get_usage_count_by_id(self, field):
        return [[k, v] for k, v in self.new_api.get_usage_count_by_id(field).iteritems()]

    def field_id_map(self, field):
        return [(k, v) for k, v in self.new_api.get_id_map(field).iteritems()]

    def get_custom_items_with_ids(self, label=None, num=None):
        try:
            return [[k, v] for k, v in self.new_api.get_id_map(self.custom_field_name(label, num)).iteritems()]
        except ValueError:
            return []

    def refresh(self, field=None, ascending=True):
        self.data.refresh(field=field, ascending=ascending)

    def get_id_from_uuid(self, uuid):
        if uuid:
            return self.new_api.lookup_by_uuid(uuid)

    def add_listener(self, listener):
        '''
        Add a listener. Will be called on change events with two arguments.
        Event name and list of affected ids.
        '''
        self.listeners.add(listener)

    def notify(self, event, ids=[]):
        'Notify all listeners'
        for listener in self.listeners:
            try:
                listener(event, ids)
            except:
                traceback.print_exc()
                continue

    # }}}

    def path(self, index, index_is_id=False):
        'Return the relative path to the directory containing this books files as a unicode string.'
        book_id = index if index_is_id else self.id(index)
        return self.new_api.field_for('path', book_id).replace('/', os.sep)

    def abspath(self, index, index_is_id=False, create_dirs=True):
        'Return the absolute path to the directory containing this books files as a unicode string.'
        path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
        if create_dirs and not os.path.exists(path):
            os.makedirs(path)
        return path

    # Adding books {{{
    def create_book_entry(self, mi, cover=None, add_duplicates=True, force_id=None):
        ret = self.new_api.create_book_entry(mi, cover=cover, add_duplicates=add_duplicates, force_id=force_id)
        if ret is not None:
            self.data.books_added((ret,))
        return ret

    def add_books(self, paths, formats, metadata, add_duplicates=True, return_ids=False):
        books = [(mi, {fmt:path}) for mi, path, fmt in zip(metadata, paths, formats)]
        book_ids, duplicates = self.new_api.add_books(books, add_duplicates=add_duplicates, dbapi=self)
        if duplicates:
            paths, formats, metadata = [], [], []
            for mi, format_map in duplicates:
                metadata.append(mi)
                for fmt, path in format_map.iteritems():
                    formats.append(fmt)
                    paths.append(path)
            duplicates = (paths, formats, metadata)
        ids = book_ids if return_ids else len(book_ids)
        if book_ids:
            self.data.books_added(book_ids)
        return duplicates or None, ids

    def import_book(self, mi, formats, notify=True, import_hooks=True, apply_import_tags=True, preserve_uuid=False):
        format_map = {}
        for path in formats:
            ext = os.path.splitext(path)[1][1:].upper()
            if ext == 'OPF':
                continue
            format_map[ext] = path
        book_ids, duplicates = self.new_api.add_books(
            [(mi, format_map)], add_duplicates=True, apply_import_tags=apply_import_tags, preserve_uuid=preserve_uuid, dbapi=self, run_hooks=import_hooks)
        if book_ids:
            self.data.books_added(book_ids)
        if notify:
            self.notify('add', book_ids)
        return book_ids[0]

    def find_books_in_directory(self, dirpath, single_book_per_directory):
        return find_books_in_directory(dirpath, single_book_per_directory)

    def import_book_directory_multiple(self, dirpath, callback=None,
            added_ids=None):
        return import_book_directory_multiple(self, dirpath, callback=callback, added_ids=added_ids)

    def import_book_directory(self, dirpath, callback=None, added_ids=None):
        return import_book_directory(self, dirpath, callback=callback, added_ids=added_ids)

    def recursive_import(self, root, single_book_per_directory=True,
            callback=None, added_ids=None):
        return recursive_import(self, root, single_book_per_directory=single_book_per_directory, callback=callback, added_ids=added_ids)

    def add_catalog(self, path, title):
        book_id, new_book_added = add_catalog(self.new_api, path, title, dbapi=self)
        if book_id is not None and new_book_added:
            self.data.books_added((book_id,))
        return book_id

    def add_news(self, path, arg):
        book_id = add_news(self.new_api, path, arg, dbapi=self)
        if book_id is not None:
            self.data.books_added((book_id,))
        return book_id

    def add_format(self, index, fmt, stream, index_is_id=False, path=None, notify=True, replace=True, copy_function=None):
        ''' path and copy_function are ignored by the new API '''
        book_id = index if index_is_id else self.id(index)
        try:
            return self.new_api.add_format(book_id, fmt, stream, replace=replace, run_hooks=False, dbapi=self)
        except:
            raise
        else:
            self.notify('metadata', [book_id])

    def add_format_with_hooks(self, index, fmt, fpath, index_is_id=False, path=None, notify=True, replace=True):
        ''' path is ignored by the new API '''
        book_id = index if index_is_id else self.id(index)
        try:
            return self.new_api.add_format(book_id, fmt, fpath, replace=replace, run_hooks=True, dbapi=self)
        except:
            raise
        else:
            self.notify('metadata', [book_id])

    # }}}

    # Custom data {{{
    def add_custom_book_data(self, book_id, name, val):
        self.new_api.add_custom_book_data(name, {book_id:val})

    def add_multiple_custom_book_data(self, name, val_map, delete_first=False):
        self.new_api.add_custom_book_data(name, val_map, delete_first=delete_first)

    def get_custom_book_data(self, book_id, name, default=None):
        return self.new_api.get_custom_book_data(name, book_ids={book_id}, default=default).get(book_id, default)

    def get_all_custom_book_data(self, name, default=None):
        return self.new_api.get_custom_book_data(name, default=default)

    def delete_custom_book_data(self, book_id, name):
        self.new_api.delete_custom_book_data(name, book_ids=(book_id,))

    def delete_all_custom_book_data(self, name):
        self.new_api.delete_custom_book_data(name)

    def get_ids_for_custom_book_data(self, name):
        return list(self.new_api.get_ids_for_custom_book_data(name))
    # }}}

    def sort(self, field, ascending, subsort=False):
        self.multisort([(field, ascending)])

    def get_field(self, index, key, default=None, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        mi = self.new_api.get_metadata(book_id, get_cover=key == 'cover')
        return mi.get(key, default)

    def cover_last_modified(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.cover_last_modified(book_id) or self.last_modified()

    def cover(self, index, index_is_id=False, as_file=False, as_image=False, as_path=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.cover(book_id, as_file=as_file, as_image=as_image, as_path=as_path)

    def copy_cover_to(self, index, dest, index_is_id=False, windows_atomic_move=None, use_hardlink=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.copy_cover_to(book_id, dest, use_hardlink=use_hardlink)

    def copy_format_to(self, index, fmt, dest, index_is_id=False, windows_atomic_move=None, use_hardlink=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.copy_format_to(book_id, fmt, dest, use_hardlink=use_hardlink)

    def delete_book(self, book_id, notify=True, commit=True, permanent=False, do_clean=True):
        self.new_api.remove_books((book_id,), permanent=permanent)
        self.data.books_deleted((book_id,))
        if notify:
            self.notify('delete', [book_id])

    def dirtied(self, book_ids, commit=True):
        self.new_api.mark_as_dirty(frozenset(book_ids) if book_ids is not None else book_ids)

    def dirty_queue_length(self):
        return self.new_api.dirty_queue_length()

    def dump_metadata(self, book_ids=None, remove_from_dirtied=True, commit=True, callback=None):
        self.new_api.dump_metadata(book_ids=book_ids, remove_from_dirtied=remove_from_dirtied, callback=callback)

    def authors_sort_strings(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return list(self.new_api.author_sort_strings_for_books((book_id,))[book_id])

    def author_sort_from_book(self, index, index_is_id=False):
        return ' & '.join(self.authors_sort_strings(index, index_is_id=index_is_id))

    def authors_with_sort_strings(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        with self.new_api.safe_read_lock:
            authors = self.new_api._field_ids_for('authors', book_id)
            adata = self.new_api._author_data(authors)
            return [(aid, adata[aid]['name'], adata[aid]['sort'], adata[aid]['link']) for aid in authors]

    def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
        changed_books = self.new_api.set_sort_for_authors({old_id:new_sort})
        if notify:
            self.notify('metadata', list(changed_books))

    def set_link_field_for_author(self, aid, link, commit=True, notify=False):
        changed_books = self.new_api.set_link_for_authors({aid:link})
        if notify:
            self.notify('metadata', list(changed_books))

    def book_on_device(self, book_id):
        with self.new_api.safe_read_lock:
            return self.new_api.fields['ondevice'].book_on_device(book_id)

    def book_on_device_string(self, book_id):
        return self.new_api.field_for('ondevice', book_id)

    def set_book_on_device_func(self, func):
        self.new_api.fields['ondevice'].set_book_on_device_func(func)

    @property
    def book_on_device_func(self):
        return self.new_api.fields['ondevice'].book_on_device_func

    def books_in_series(self, series_id):
        with self.new_api.safe_read_lock:
            book_ids = self.new_api._books_for_field('series', series_id)
            ff = self.new_api._field_for
            return sorted(book_ids, key=lambda x:ff('series_index', x))

    def books_in_series_of(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        series_ids = self.new_api.field_ids_for('series', book_id)
        if not series_ids:
            return []
        return self.books_in_series(series_ids[0])

    def books_with_same_title(self, mi, all_matches=True):
        title = mi.title
        ans = set()
        if title:
            title = icu_lower(force_unicode(title))
            for book_id, x in self.new_api.get_id_map('title').iteritems():
                if icu_lower(x) == title:
                    ans.add(book_id)
                    if not all_matches:
                        break
        return ans

    def set_conversion_options(self, book_id, fmt, options):
        self.new_api.set_conversion_options({book_id:options}, fmt=fmt)

    def conversion_options(self, book_id, fmt):
        return self.new_api.conversion_options(book_id, fmt=fmt)

    def has_conversion_options(self, ids, format='PIPE'):
        return self.new_api.has_conversion_options(ids, fmt=format)

    def delete_conversion_options(self, book_id, fmt, commit=True):
        self.new_api.delete_conversion_options((book_id,), fmt=fmt)

    def set(self, index, field, val, allow_case_change=False):
        book_id = self.id(index)
        try:
            return self.new_api.set_field(field, {book_id:val}, allow_case_change=allow_case_change)
        finally:
            self.notify('metadata', [book_id])

    def set_identifier(self, book_id, typ, val, notify=True, commit=True):
        with self.new_api.write_lock:
            identifiers = self.new_api._field_for('identifiers', book_id)
            typ, val = clean_identifier(typ, val)
            if typ:
                identifiers[typ] = val
                self.new_api._set_field('identifiers', {book_id:identifiers})
        self.notify('metadata', [book_id])

    def set_isbn(self, book_id, isbn, notify=True, commit=True):
        self.set_identifier(book_id, 'isbn', isbn, notify=notify, commit=commit)

    def set_tags(self, book_id, tags, append=False, notify=True, commit=True, allow_case_change=False):
        tags = tags or []
        with self.new_api.write_lock:
            if append:
                otags = self.new_api._field_for('tags', book_id)
                existing = {icu_lower(x) for x in otags}
                tags = list(otags) + [x for x in tags if icu_lower(x) not in existing]
            ret = self.new_api._set_field('tags', {book_id:tags}, allow_case_change=allow_case_change)
        if notify:
            self.notify('metadata', [book_id])
        return ret

    def set_metadata(self, book_id, mi, ignore_errors=False, set_title=True,
                     set_authors=True, commit=True, force_changes=False, notify=True):
        self.new_api.set_metadata(book_id, mi, ignore_errors=ignore_errors, set_title=set_title, set_authors=set_authors, force_changes=force_changes)
        if notify:
            self.notify('metadata', [book_id])

    def remove_all_tags(self, ids, notify=False, commit=True):
        self.new_api.set_field('tags', {book_id:() for book_id in ids})
        if notify:
            self.notify('metadata', ids)

    def _do_bulk_modify(self, field, ids, add, remove, notify):
        add = cleanup_tags(add)
        remove = cleanup_tags(remove)
        remove = set(remove) - set(add)
        if not ids or (not add and not remove):
            return

        remove = {icu_lower(x) for x in remove}
        with self.new_api.write_lock:
            val_map = {}
            for book_id in ids:
                tags = list(self.new_api._field_for(field, book_id))
                existing = {icu_lower(x) for x in tags}
                tags.extend(t for t in add if icu_lower(t) not in existing)
                tags = tuple(t for t in tags if icu_lower(t) not in remove)
                val_map[book_id] = tags
            self.new_api._set_field(field, val_map, allow_case_change=False)

        if notify:
            self.notify('metadata', ids)

    def bulk_modify_tags(self, ids, add=[], remove=[], notify=False):
        self._do_bulk_modify('tags', ids, add, remove, notify)

    def set_custom_bulk_multiple(self, ids, add=[], remove=[], label=None, num=None, notify=False):
        data = self.backend.custom_field_metadata(label, num)
        if not data['editable']:
            raise ValueError('Column %r is not editable'%data['label'])
        if data['datatype'] != 'text' or not data['is_multiple']:
            raise ValueError('Column %r is not text/multiple'%data['label'])
        field = self.custom_field_name(label, num)
        self._do_bulk_modify(field, ids, add, remove, notify)

    def unapply_tags(self, book_id, tags, notify=True):
        self.bulk_modify_tags((book_id,), remove=tags, notify=notify)

    def is_tag_used(self, tag):
        return icu_lower(tag) in {icu_lower(x) for x in self.new_api.all_field_names('tags')}

    def delete_tag(self, tag):
        self.delete_tags((tag,))

    def delete_tags(self, tags):
        with self.new_api.write_lock:
            tag_map = {icu_lower(v):k for k, v in self.new_api._get_id_map('tags').iteritems()}
            tag_ids = (tag_map.get(icu_lower(tag), None) for tag in tags)
            tag_ids = tuple(tid for tid in tag_ids if tid is not None)
            if tag_ids:
                self.new_api._remove_items('tags', tag_ids)

    def has_id(self, book_id):
        return self.new_api.has_id(book_id)

    def format(self, index, fmt, index_is_id=False, as_file=False, mode='r+b', as_path=False, preserve_filename=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format(book_id, fmt, as_file=as_file, as_path=as_path, preserve_filename=preserve_filename)

    def format_abspath(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format_abspath(book_id, fmt)

    def format_path(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.format_abspath(book_id, fmt)
        if ans is None:
            raise NoSuchFormat('Record %d has no format: %s'%(book_id, fmt))
        return ans

    def format_files(self, index, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return [(v, k) for k, v in self.new_api.format_files(book_id).iteritems()]

    def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False, commit=False):
        return self.new_api.format_metadata(book_id, fmt, allow_cache=allow_cache, update_db=update_db)

    def format_last_modified(self, book_id, fmt):
        m = self.format_metadata(book_id, fmt)
        if m:
            return m['mtime']

    def formats(self, index, index_is_id=False, verify_formats=True):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.formats(book_id, verify_formats=verify_formats)
        if ans:
            return ','.join(ans)

    def has_format(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.has_format(book_id, fmt)

    def refresh_format_cache(self):
        self.new_api.refresh_format_cache()

    def refresh_ondevice(self):
        self.new_api.refresh_ondevice()

    def tags_older_than(self, tag, delta, must_have_tag=None, must_have_authors=None):
        for book_id in sorted(self.new_api.tags_older_than(tag, delta=delta, must_have_tag=must_have_tag, must_have_authors=must_have_authors)):
            yield book_id

    def sizeof_format(self, index, fmt, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.format_metadata(book_id, fmt).get('size', None)

    def get_metadata(self, index, index_is_id=False, get_cover=False, get_user_categories=True, cover_as_data=False):
        book_id = index if index_is_id else self.id(index)
        return self.new_api.get_metadata(book_id, get_cover=get_cover, get_user_categories=get_user_categories, cover_as_data=cover_as_data)

    def rename_series(self, old_id, new_name, change_index=True):
        self.new_api.rename_items('series', {old_id:new_name}, change_index=change_index)

    def get_custom(self, index, label=None, num=None, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        ans = self.new_api.field_for(self.custom_field_name(label, num), book_id)
        if isinstance(ans, tuple):
            ans = list(ans)
        return ans

    def get_custom_extra(self, index, label=None, num=None, index_is_id=False):
        data = self.backend.custom_field_metadata(label, num)
        # add future datatypes with an extra column here
        if data['datatype'] != 'series':
            return None
        book_id = index if index_is_id else self.id(index)
        return self.new_api.field_for(self.custom_field_name(label, num) + '_index', book_id)

    def get_custom_and_extra(self, index, label=None, num=None, index_is_id=False):
        book_id = index if index_is_id else self.id(index)
        data = self.backend.custom_field_metadata(label, num)
        ans = self.new_api.field_for(self.custom_field_name(label, num), book_id)
        if isinstance(ans, tuple):
            ans = list(ans)
        if data['datatype'] != 'series':
            return (ans, None)
        return (ans, self.new_api.field_for(self.custom_field_name(label, num) + '_index', book_id))

    def get_next_cc_series_num_for(self, series, label=None, num=None):
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] != 'series':
            return None
        return self.new_api.get_next_series_num_for(series, field=self.custom_field_name(label, num))

    def is_item_used_in_multiple(self, item, label=None, num=None):
        existing_tags = self.all_custom(label=label, num=num)
        return icu_lower(item) in {icu_lower(t) for t in existing_tags}

    def delete_custom_item_using_id(self, item_id, label=None, num=None):
        self.new_api.remove_items(self.custom_field_name(label, num), (item_id,))

    def rename_custom_item(self, old_id, new_name, label=None, num=None):
        self.new_api.rename_items(self.custom_field_name(label, num), {old_id:new_name}, change_index=False)

    def delete_item_from_multiple(self, item, label=None, num=None):
        field = self.custom_field_name(label, num)
        existing = self.new_api.get_id_map(field)
        rmap = {icu_lower(v):k for k, v in existing.iteritems()}
        item_id = rmap.get(icu_lower(item), None)
        if item_id is None:
            return []
        return list(self.new_api.remove_items(field, (item_id,)))

    def set_custom(self, book_id, val, label=None, num=None, append=False,
                   notify=True, extra=None, commit=True, allow_case_change=False):
        field = self.custom_field_name(label, num)
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] == 'composite':
            return set()
        if not data['editable']:
            raise ValueError('Column %r is not editable'%data['label'])
        if data['datatype'] == 'enumeration' and (
                val and val not in data['display']['enum_values']):
            return set()
        with self.new_api.write_lock:
            if append and data['is_multiple']:
                current = self.new_api._field_for(field, book_id)
                existing = {icu_lower(x) for x in current}
                val = current + tuple(x for x in self.new_api.fields[field].writer.adapter(val) if icu_lower(x) not in existing)
                affected_books = self.new_api._set_field(field, {book_id:val}, allow_case_change=allow_case_change)
            else:
                affected_books = self.new_api._set_field(field, {book_id:val}, allow_case_change=allow_case_change)
            if data['datatype'] == 'series':
                s, sidx = get_series_values(val)
                if sidx is None:
                    extra = 1.0 if extra is None else extra
                    self.new_api._set_field(field + '_index', {book_id:extra})
        if notify and affected_books:
            self.notify('metadata', list(affected_books))
        return affected_books

    def set_custom_bulk(self, ids, val, label=None, num=None,
                   append=False, notify=True, extras=None):
        if extras is not None and len(extras) != len(ids):
            raise ValueError('Length of ids and extras is not the same')
        field = self.custom_field_name(label, num)
        data = self.backend.custom_field_metadata(label, num)
        if data['datatype'] == 'composite':
            return set()
        if data['datatype'] == 'enumeration' and (
                val and val not in data['display']['enum_values']):
            return
        if not data['editable']:
            raise ValueError('Column %r is not editable'%data['label'])

        if append:
            for book_id in ids:
                self.set_custom(book_id, val, label=label, num=num, append=True, notify=False)
        else:
            with self.new_api.write_lock:
                self.new_api._set_field(field, {book_id:val for book_id in ids}, allow_case_change=False)
            if extras is not None:
                self.new_api._set_field(field + '_index', {book_id:val for book_id, val in zip(ids, extras)})
        if notify:
            self.notify('metadata', list(ids))

    def delete_custom_column(self, label=None, num=None):
        self.new_api.delete_custom_column(label, num)

    def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}):
        self.new_api.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display)

    def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None,
                                   notify=True, update_last_modified=False):
        changed = self.new_api.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable,
                                                          display=display, update_last_modified=update_last_modified)
        if changed and notify:
            self.notify('metadata', [])

    def remove_cover(self, book_id, notify=True, commit=True):
        self.new_api.set_cover({book_id:None})
        if notify:
            self.notify('cover', [book_id])

    def set_cover(self, book_id, data, notify=True, commit=True):
        self.new_api.set_cover({book_id:data})
        if notify:
            self.notify('cover', [book_id])

    def original_fmt(self, book_id, fmt):
        nfmt = ('ORIGINAL_%s'%fmt).upper()
        return nfmt if self.new_api.has_format(book_id, nfmt) else fmt

    def save_original_format(self, book_id, fmt, notify=True):
        ret = self.new_api.save_original_format(book_id, fmt)
        if ret and notify:
            self.notify('metadata', [book_id])
        return ret

    def restore_original_format(self, book_id, original_fmt, notify=True):
        ret = self.new_api.restore_original_format(book_id, original_fmt)
        if ret and notify:
            self.notify('metadata', [book_id])
        return ret

    def remove_format(self, index, fmt, index_is_id=False, notify=True, commit=True, db_only=False):
        book_id = index if index_is_id else self.id(index)
        self.new_api.remove_formats({book_id:(fmt,)}, db_only=db_only)
        if notify:
            self.notify('metadata', [book_id])

    # Private interface {{{
    def __iter__(self):
        for row in self.data.iterall():
            yield row

    def _get_next_series_num_for_list(self, series_indices):
        return _get_next_series_num_for_list(series_indices)

    def _get_series_values(self, val):
        return _get_series_values(val)
Exemplo n.º 3
0
class LibraryDatabase(object):

    ''' Emulate the old LibraryDatabase2 interface '''

    PATH_LIMIT = DB.PATH_LIMIT
    WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT
    CATEGORY_SORTS = CATEGORY_SORTS
    MATCH_TYPE = ('any', 'all')
    CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
        'int', 'float', 'bool', 'series', 'composite', 'enumeration'])

    @classmethod
    def exists_at(cls, path):
        return path and os.path.exists(os.path.join(path, 'metadata.db'))

    def __init__(self, library_path,
            default_prefs=None, read_only=False, is_second_db=False,
            progress_callback=lambda x, y:True, restore_all_prefs=False):

        self.is_second_db = is_second_db  # TODO: Use is_second_db
        self.listeners = set()

        backend = self.backend = DB(library_path, default_prefs=default_prefs,
                     read_only=read_only, restore_all_prefs=restore_all_prefs,
                     progress_callback=progress_callback)
        cache = self.new_api = Cache(backend)
        cache.init()
        self.data = View(cache)

        self.get_property = self.data.get_property

        for prop in (
                'author_sort', 'authors', 'comment', 'comments',
                'publisher', 'rating', 'series', 'series_index', 'tags',
                'title', 'timestamp', 'uuid', 'pubdate', 'ondevice',
                'metadata_last_modified', 'languages',
                ):
            fm = {'comment':'comments', 'metadata_last_modified':
                  'last_modified', 'title_sort':'sort'}.get(prop, prop)
            setattr(self, prop, partial(self.get_property,
                    loc=self.FIELD_MAP[fm]))

        for meth in ('get_next_series_num_for', 'has_book', 'author_sort_from_authors'):
            setattr(self, meth, getattr(self.new_api, meth))

        self.last_update_check = self.last_modified()

    def close(self):
        self.backend.close()

    def break_cycles(self):
        delattr(self.backend, 'field_metadata')
        self.data.cache.backend = None
        self.data.cache = None
        for x in ('data', 'backend', 'new_api', 'listeners',):
            delattr(self, x)

    # Library wide properties {{{
    @property
    def field_metadata(self):
        return self.backend.field_metadata

    @property
    def user_version(self):
        return self.backend.user_version

    @property
    def library_id(self):
        return self.backend.library_id

    @property
    def library_path(self):
        return self.backend.library_path

    @property
    def dbpath(self):
        return self.backend.dbpath

    def last_modified(self):
        return self.backend.last_modified()

    def check_if_modified(self):
        if self.last_modified() > self.last_update_check:
            self.refresh()
        self.last_update_check = utcnow()

    @property
    def custom_column_num_map(self):
        return self.backend.custom_column_num_map

    @property
    def custom_column_label_map(self):
        return self.backend.custom_column_label_map

    @property
    def FIELD_MAP(self):
        return self.backend.FIELD_MAP

    @property
    def formatter_template_cache(self):
        return self.data.cache.formatter_template_cache

    def initialize_template_cache(self):
        self.data.cache.initialize_template_cache()

    def all_ids(self):
        for book_id in self.data.cache.all_book_ids():
            yield book_id

    def refresh(self, field=None, ascending=True):
        self.data.cache.refresh()
        self.data.refresh(field=field, ascending=ascending)

    def add_listener(self, listener):
        '''
        Add a listener. Will be called on change events with two arguments.
        Event name and list of affected ids.
        '''
        self.listeners.add(listener)

    def notify(self, event, ids=[]):
        'Notify all listeners'
        for listener in self.listeners:
            try:
                listener(event, ids)
            except:
                traceback.print_exc()
                continue

    # }}}

    def path(self, index, index_is_id=False):
        'Return the relative path to the directory containing this books files as a unicode string.'
        book_id = index if index_is_id else self.data.index_to_id(index)
        return self.new_api.field_for('path', book_id).replace('/', os.sep)

    def abspath(self, index, index_is_id=False, create_dirs=True):
        'Return the absolute path to the directory containing this books files as a unicode string.'
        path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
        if create_dirs and not os.path.exists(path):
            os.makedirs(path)
        return path

    # Adding books {{{
    def create_book_entry(self, mi, cover=None, add_duplicates=True, force_id=None):
        return self.new_api.create_book_entry(mi, cover=cover, add_duplicates=add_duplicates, force_id=force_id)

    def add_books(self, paths, formats, metadata, add_duplicates=True, return_ids=False):
        books = [(mi, {fmt:path}) for mi, path, fmt in zip(metadata, paths, formats)]
        book_ids, duplicates = self.new_api.add_books(books, add_duplicates=add_duplicates, dbapi=self)
        if duplicates:
            paths, formats, metadata = [], [], []
            for mi, format_map in duplicates:
                metadata.append(mi)
                for fmt, path in format_map.iteritems():
                    formats.append(fmt)
                    paths.append(path)
            duplicates = (paths, formats, metadata)
        ids = book_ids if return_ids else len(book_ids)
        return duplicates or None, ids

    def import_book(self, mi, formats, notify=True, import_hooks=True, apply_import_tags=True, preserve_uuid=False):
        format_map = {}
        for path in formats:
            ext = os.path.splitext(path)[1][1:].upper()
            if ext == 'OPF':
                continue
            format_map[ext] = path
        book_ids, duplicates = self.new_api.add_books(
            [(mi, format_map)], add_duplicates=True, apply_import_tags=apply_import_tags, preserve_uuid=preserve_uuid, dbapi=self, run_hooks=import_hooks)
        if notify:
            self.notify('add', book_ids)
        return book_ids[0]

    def find_books_in_directory(self, dirpath, single_book_per_directory):
        return find_books_in_directory(dirpath, single_book_per_directory)

    def import_book_directory_multiple(self, dirpath, callback=None,
            added_ids=None):
        return import_book_directory_multiple(self, dirpath, callback=callback, added_ids=added_ids)

    def import_book_directory(self, dirpath, callback=None, added_ids=None):
        return import_book_directory(self, dirpath, callback=callback, added_ids=added_ids)

    def recursive_import(self, root, single_book_per_directory=True,
            callback=None, added_ids=None):
        return recursive_import(self, root, single_book_per_directory=single_book_per_directory, callback=callback, added_ids=added_ids)
    # }}}

    # Private interface {{{

    def __iter__(self):
        for row in self.data.iterall():
            yield row

    def _get_next_series_num_for_list(self, series_indices):
        return _get_next_series_num_for_list(series_indices)

    def _get_series_values(self, val):
        return _get_series_values(val)
Exemplo n.º 4
0
class LibraryDatabase(object):

    ''' Emulate the old LibraryDatabase2 interface '''

    PATH_LIMIT = DB.PATH_LIMIT
    WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT
    CATEGORY_SORTS = CATEGORY_SORTS
    MATCH_TYPE = ('any', 'all')
    CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
        'int', 'float', 'bool', 'series', 'composite', 'enumeration'])

    @classmethod
    def exists_at(cls, path):
        return path and os.path.exists(os.path.join(path, 'metadata.db'))

    def __init__(self, library_path,
            default_prefs=None, read_only=False, is_second_db=False,
            progress_callback=lambda x, y:True, restore_all_prefs=False):

        self.is_second_db = is_second_db  # TODO: Use is_second_db
        self.listeners = set([])

        backend = self.backend = DB(library_path, default_prefs=default_prefs,
                     read_only=read_only, restore_all_prefs=restore_all_prefs,
                     progress_callback=progress_callback)
        cache = self.new_api = Cache(backend)
        cache.init()
        self.data = View(cache)

        self.get_property = self.data.get_property

        for prop in (
                'author_sort', 'authors', 'comment', 'comments',
                'publisher', 'rating', 'series', 'series_index', 'tags',
                'title', 'timestamp', 'uuid', 'pubdate', 'ondevice',
                'metadata_last_modified', 'languages',
                ):
            fm = {'comment':'comments', 'metadata_last_modified':
                  'last_modified', 'title_sort':'sort'}.get(prop, prop)
            setattr(self, prop, partial(self.get_property,
                    loc=self.FIELD_MAP[fm]))

        self.last_update_check = self.last_modified()

    def close(self):
        self.backend.close()

    def break_cycles(self):
        self.data.cache.backend = None
        self.data.cache = None
        self.data = self.backend = self.new_api = self.field_metadata = self.prefs = self.listeners = self.refresh_ondevice = None

    # Library wide properties {{{
    @property
    def field_metadata(self):
        return self.backend.field_metadata

    @property
    def user_version(self):
        return self.backend.user_version

    @property
    def library_id(self):
        return self.backend.library_id

    @property
    def library_path(self):
        return self.backend.library_path

    @property
    def dbpath(self):
        return self.backend.dbpath

    def last_modified(self):
        return self.backend.last_modified()

    def check_if_modified(self):
        if self.last_modified() > self.last_update_check:
            self.refresh()
        self.last_update_check = utcnow()

    @property
    def custom_column_num_map(self):
        return self.backend.custom_column_num_map

    @property
    def custom_column_label_map(self):
        return self.backend.custom_column_label_map

    @property
    def FIELD_MAP(self):
        return self.backend.FIELD_MAP

    @property
    def formatter_template_cache(self):
        return self.data.cache.formatter_template_cache

    def initialize_template_cache(self):
        self.data.cache.initialize_template_cache()

    def all_ids(self):
        for book_id in self.data.cache.all_book_ids():
            yield book_id

    def refresh(self, field=None, ascending=True):
        self.data.cache.refresh()
        self.data.refresh(field=field, ascending=ascending)

    def add_listener(self, listener):
        '''
        Add a listener. Will be called on change events with two arguments.
        Event name and list of affected ids.
        '''
        self.listeners.add(listener)

    def notify(self, event, ids=[]):
        'Notify all listeners'
        for listener in self.listeners:
            try:
                listener(event, ids)
            except:
                traceback.print_exc()
                continue

    # }}}

    def path(self, index, index_is_id=False):
        'Return the relative path to the directory containing this books files as a unicode string.'
        book_id = index if index_is_id else self.data.index_to_id(index)
        return self.data.cache.field_for('path', book_id).replace('/', os.sep)

    def abspath(self, index, index_is_id=False, create_dirs=True):
        'Return the absolute path to the directory containing this books files as a unicode string.'
        path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
        if create_dirs and not os.path.exists(path):
            os.makedirs(path)
        return path

    # Private interface {{{

    def __iter__(self):
        for row in self.data.iterall():
            yield row

    def _get_next_series_num_for_list(self, series_indices):
        return _get_next_series_num_for_list(series_indices)

    def _get_series_values(self, val):
        return _get_series_values(val)
Exemplo n.º 5
0
class LibraryDatabase(object):

    """ Emulate the old LibraryDatabase2 interface """

    PATH_LIMIT = DB.PATH_LIMIT
    WINDOWS_LIBRARY_PATH_LIMIT = DB.WINDOWS_LIBRARY_PATH_LIMIT
    CATEGORY_SORTS = CATEGORY_SORTS
    MATCH_TYPE = ("any", "all")
    CUSTOM_DATA_TYPES = frozenset(
        ["rating", "text", "comments", "datetime", "int", "float", "bool", "series", "composite", "enumeration"]
    )

    @classmethod
    def exists_at(cls, path):
        return path and os.path.exists(os.path.join(path, "metadata.db"))

    def __init__(
        self,
        library_path,
        default_prefs=None,
        read_only=False,
        is_second_db=False,
        progress_callback=lambda x, y: True,
        restore_all_prefs=False,
    ):

        self.is_second_db = is_second_db  # TODO: Use is_second_db
        self.listeners = set([])

        backend = self.backend = DB(
            library_path,
            default_prefs=default_prefs,
            read_only=read_only,
            restore_all_prefs=restore_all_prefs,
            progress_callback=progress_callback,
        )
        cache = self.new_api = Cache(backend)
        cache.init()
        self.data = View(cache)

        self.get_property = self.data.get_property

        for prop in (
            "author_sort",
            "authors",
            "comment",
            "comments",
            "publisher",
            "rating",
            "series",
            "series_index",
            "tags",
            "title",
            "timestamp",
            "uuid",
            "pubdate",
            "ondevice",
            "metadata_last_modified",
            "languages",
        ):
            fm = {"comment": "comments", "metadata_last_modified": "last_modified", "title_sort": "sort"}.get(
                prop, prop
            )
            setattr(self, prop, partial(self.get_property, loc=self.FIELD_MAP[fm]))

        self.last_update_check = self.last_modified()

    def close(self):
        self.backend.close()

    def break_cycles(self):
        self.data.cache.backend = None
        self.data.cache = None
        self.data = (
            self.backend
        ) = self.new_api = self.field_metadata = self.prefs = self.listeners = self.refresh_ondevice = None

    # Library wide properties {{{
    @property
    def field_metadata(self):
        return self.backend.field_metadata

    @property
    def user_version(self):
        return self.backend.user_version

    @property
    def library_id(self):
        return self.backend.library_id

    @property
    def library_path(self):
        return self.backend.library_path

    @property
    def dbpath(self):
        return self.backend.dbpath

    def last_modified(self):
        return self.backend.last_modified()

    def check_if_modified(self):
        if self.last_modified() > self.last_update_check:
            self.refresh()
        self.last_update_check = utcnow()

    @property
    def custom_column_num_map(self):
        return self.backend.custom_column_num_map

    @property
    def custom_column_label_map(self):
        return self.backend.custom_column_label_map

    @property
    def FIELD_MAP(self):
        return self.backend.FIELD_MAP

    @property
    def formatter_template_cache(self):
        return self.data.cache.formatter_template_cache

    def initialize_template_cache(self):
        self.data.cache.initialize_template_cache()

    def all_ids(self):
        for book_id in self.data.cache.all_book_ids():
            yield book_id

    def refresh(self, field=None, ascending=True):
        self.data.cache.refresh()
        self.data.refresh(field=field, ascending=ascending)

    def add_listener(self, listener):
        """
        Add a listener. Will be called on change events with two arguments.
        Event name and list of affected ids.
        """
        self.listeners.add(listener)

    def notify(self, event, ids=[]):
        "Notify all listeners"
        for listener in self.listeners:
            try:
                listener(event, ids)
            except:
                traceback.print_exc()
                continue

    # }}}

    def path(self, index, index_is_id=False):
        "Return the relative path to the directory containing this books files as a unicode string."
        book_id = index if index_is_id else self.data.index_to_id(index)
        return self.data.cache.field_for("path", book_id).replace("/", os.sep)

    def abspath(self, index, index_is_id=False, create_dirs=True):
        "Return the absolute path to the directory containing this books files as a unicode string."
        path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
        if create_dirs and not os.path.exists(path):
            os.makedirs(path)
        return path

    # Private interface {{{

    def __iter__(self):
        for row in self.data.iterall():
            yield row

    def _get_next_series_num_for_list(self, series_indices):
        return _get_next_series_num_for_list(series_indices)

    def _get_series_values(self, val):
        return _get_series_values(val)