Exemplo n.º 1
0
 def parse_metadata_cache(cls, bl, prefix, name):
     json_codec = JsonCodec()
     need_sync = False
     cache_file = cls.normalize_path(os.path.join(prefix, name))
     if os.access(cache_file, os.R_OK):
         try:
             with lopen(cache_file, 'rb') as f:
                 json_codec.decode_from_file(f, bl, cls.book_class, prefix)
         except:
             import traceback
             traceback.print_exc()
             bl = []
             need_sync = True
     else:
         need_sync = True
     return need_sync
Exemplo n.º 2
0
 def parse_metadata_cache(cls, bl, prefix, name):
     json_codec = JsonCodec()
     need_sync = False
     cache_file = cls.normalize_path(os.path.join(prefix, name))
     if os.access(cache_file, os.R_OK):
         try:
             with lopen(cache_file, 'rb') as f:
                 json_codec.decode_from_file(f, bl, cls.book_class, prefix)
         except:
             import traceback
             traceback.print_exc()
             bl = []
             need_sync = True
     else:
         need_sync = True
     return need_sync
Exemplo n.º 3
0
    def sync_booklists(self, booklists, end_session=True):
        debug_print('USBMS: starting sync_booklists')
        json_codec = JsonCodec()

        if not os.path.exists(self.normalize_path(self._main_prefix)):
            os.makedirs(self.normalize_path(self._main_prefix))

        def write_prefix(prefix, listid):
            if (prefix is not None and len(booklists) > listid
                    and isinstance(booklists[listid], self.booklist_class)):
                if not os.path.exists(prefix):
                    os.makedirs(self.normalize_path(prefix))
                with lopen(
                        self.normalize_path(
                            os.path.join(prefix, self.METADATA_CACHE)),
                        'wb') as f:
                    json_codec.encode_to_file(f, booklists[listid])
                    fsync(f)

        write_prefix(self._main_prefix, 0)
        write_prefix(self._card_a_prefix, 1)
        write_prefix(self._card_b_prefix, 2)

        # Clear the _new_book indication, as we are supposed to be done with
        # adding books at this point
        for blist in booklists:
            if blist is not None:
                for book in blist:
                    book._new_book = False

        self.report_progress(1.0, _('Sending metadata to device...'))
        debug_print('USBMS: finished sync_booklists')
Exemplo n.º 4
0
    def parse_metadata_cache(self, bl):
        need_sync = True
        if not self.bambook:
            return need_sync

        # Get the metadata virtual book from Bambook
        with TemporaryDirectory() as tdir:
            if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir):
                cache_name = os.path.join(tdir, self.METADATA_CACHE)
                if self.bambook.ExtractSNBContent(os.path.join(tdir, self.METADATA_FILE_GUID),
                                                  'snbc/' + self.METADATA_CACHE,
                                                  cache_name):
                    json_codec = JsonCodec()
                    if os.access(cache_name, os.R_OK):
                        try:
                            with open(cache_name, 'rb') as f:
                                json_codec.decode_from_file(f, bl, self.book_class, '')
                                need_sync = False
                        except:
                            import traceback
                            traceback.print_exc()
                            bl = []
        return need_sync
Exemplo n.º 5
0
    def parse_metadata_cache(self, bl):
        need_sync = True
        if not self.bambook:
            return need_sync

        # Get the metadata virtual book from Bambook
        with TemporaryDirectory() as tdir:
            if self.bambook.GetFile(self.METADATA_FILE_GUID, tdir):
                cache_name = os.path.join(tdir, self.METADATA_CACHE)
                if self.bambook.ExtractSNBContent(
                        os.path.join(tdir, self.METADATA_FILE_GUID),
                        'snbc/' + self.METADATA_CACHE, cache_name):
                    json_codec = JsonCodec()
                    if os.access(cache_name, os.R_OK):
                        try:
                            with open(cache_name, 'rb') as f:
                                json_codec.decode_from_file(
                                    f, bl, self.book_class, '')
                                need_sync = False
                        except:
                            import traceback
                            traceback.print_exc()
                            bl = []
        return need_sync
Exemplo n.º 6
0
 def __init__(self):
     self.ajax_json_codec = JsonCodec()
Exemplo n.º 7
0
class AjaxServer(object):
    def __init__(self):
        self.ajax_json_codec = JsonCodec()

    def add_routes(self, connect):
        base_href = '/ajax'

        # Metadata for books
        connect('ajax_book', base_href + '/book/{book_id}', self.ajax_book)
        connect('ajax_books', base_href + '/books', self.ajax_books)

        # The list of top level categories
        connect('ajax_categories', base_href + '/categories',
                self.ajax_categories)

        # The list of sub-categories and items in each category
        connect('ajax_category', base_href + '/category/{name}',
                self.ajax_category)

        # List of books in specified category
        connect('ajax_books_in', base_href + '/books_in/{category}/{item}',
                self.ajax_books_in)

        # Search
        connect('ajax_search', base_href + '/search', self.ajax_search)

    # Get book metadata {{{
    def ajax_book_to_json(self,
                          book_id,
                          get_category_urls=True,
                          device_compatible=False,
                          device_for_template=None):
        mi = self.db.get_metadata(book_id, index_is_id=True)

        if not device_compatible:
            try:
                mi.rating = mi.rating / 2.
            except:
                mi.rating = 0.0

        data = self.ajax_json_codec.encode_book_metadata(mi)
        for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime',
                  'rights', 'book_producer'):
            data.pop(x, None)

        data['cover'] = absurl(self.opts.url_prefix,
                               u'/get/cover/%d' % book_id)
        data['thumbnail'] = absurl(self.opts.url_prefix,
                                   u'/get/thumb/%d' % book_id)

        if not device_compatible:
            mi.format_metadata = {
                k.lower(): dict(v)
                for k, v in mi.format_metadata.iteritems()
            }
            for v in mi.format_metadata.itervalues():
                mtime = v.get('mtime', None)
                if mtime is not None:
                    v['mtime'] = isoformat(mtime, as_utc=True)
            data['format_metadata'] = mi.format_metadata
            fmts = set(x.lower() for x in mi.format_metadata.iterkeys())
            pf = prefs['output_format'].lower()
            other_fmts = list(fmts)
            try:
                fmt = pf if pf in fmts else other_fmts[0]
            except:
                fmt = None
            if fmts and fmt:
                other_fmts = [x for x in fmts if x != fmt]
            data['formats'] = sorted(fmts)
            if fmt:
                data['main_format'] = {
                    fmt:
                    absurl(self.opts.url_prefix,
                           u'/get/%s/%d' % (fmt, book_id))
                }
            else:
                data['main_format'] = None
            data['other_formats'] = {
                fmt: absurl(self.opts.url_prefix,
                            u'/get/%s/%d' % (fmt, book_id))
                for fmt in other_fmts
            }

            if get_category_urls:
                category_urls = data['category_urls'] = {}
                ccache = self.categories_cache()
                for key in mi.all_field_keys():
                    fm = mi.metadata_for_field(key)
                    if (fm and fm['is_category'] and not fm['is_csp']
                            and key != 'formats'
                            and fm['datatype'] not in ['rating']):
                        categories = mi.get(key)
                        if isinstance(categories, basestring):
                            categories = [categories]
                        if categories is None:
                            categories = []
                        dbtags = {}
                        for category in categories:
                            for tag in ccache.get(key, []):
                                if tag.original_name == category:
                                    dbtags[category] = books_in_url(
                                        self.opts.url_prefix,
                                        tag.category if tag.category else key,
                                        tag.original_name
                                        if tag.id is None else unicode(tag.id))
                                    break
                        category_urls[key] = dbtags
        else:
            series = data.get('series', None)
            if series:
                tsorder = tweaks['save_template_title_series_sorting']
                series = title_sort(series, order=tsorder)
            else:
                series = ''
            data['_series_sort_'] = series
            if device_for_template:
                import posixpath
                from calibre.devices.utils import create_upload_path
                from calibre.utils.filenames import ascii_filename as sanitize
                from calibre.customize.ui import device_plugins

                for device_class in device_plugins():
                    if device_class.__class__.__name__ == device_for_template:
                        template = device_class.save_template()
                        data['_filename_'] = create_upload_path(
                            mi,
                            book_id,
                            template,
                            sanitize,
                            path_type=posixpath)
                        break

        return data, mi.last_modified

    @Endpoint(set_last_modified=False)
    def ajax_book(self,
                  book_id,
                  category_urls='true',
                  id_is_uuid='false',
                  device_compatible='false',
                  device_for_template=None):
        '''
        Return the metadata of the book as a JSON dictionary.

        If category_urls == 'true' the returned dictionary also contains a
        mapping of category names to URLs that return the list of books in the
        given category.
        '''
        cherrypy.response.timeout = 3600

        try:
            if id_is_uuid == 'true':
                book_id = self.db.get_id_from_uuid(book_id)
            else:
                book_id = int(book_id)
            data, last_modified = self.ajax_book_to_json(
                book_id,
                get_category_urls=category_urls.lower() == 'true',
                device_compatible=device_compatible.lower() == 'true',
                device_for_template=device_for_template)
        except:
            raise cherrypy.HTTPError(404, 'No book with id: %r' % book_id)

        cherrypy.response.headers['Last-Modified'] = \
                self.last_modified(last_modified)

        return data

    @Endpoint(set_last_modified=False)
    def ajax_books(self,
                   ids=None,
                   category_urls='true',
                   id_is_uuid='false',
                   device_for_template=None):
        '''
        Return the metadata for a list of books specified as a comma separated
        list of ids. The metadata is returned as a dictionary mapping ids to
        the metadata. The format for the metadata is the same as in
        ajax_book(). If no book is found for a given id, it is mapped to null
        in the dictionary.

        This endpoint can be used with either GET or POST requests, variable
        name is ids: /ajax/books?ids=1,2,3,4,5
        '''
        if ids is None:
            raise cherrypy.HTTPError(404, 'Must specify some ids')
        try:
            if id_is_uuid == 'true':
                ids = set(self.db.get_id_from_uuid(x) for x in ids.split(','))
            else:
                ids = set(int(x.strip()) for x in ids.split(','))
        except:
            raise cherrypy.HTTPError(
                404, 'ids must be a comma separated list'
                ' of integers')
        ans = {}
        lm = None
        gcu = category_urls.lower() == 'true'
        for book_id in ids:
            try:
                data, last_modified = self.ajax_book_to_json(
                    book_id,
                    get_category_urls=gcu,
                    device_for_template=device_for_template)
            except:
                ans[book_id] = None
            else:
                ans[book_id] = data
                if lm is None or last_modified > lm:
                    lm = last_modified

        cherrypy.response.timeout = 3600
        cherrypy.response.headers['Last-Modified'] = \
                self.last_modified(lm if lm is not None else
                        self.db.last_modified())

        return ans

    # }}}

    # Top level categories {{{
    @Endpoint()
    def ajax_categories(self):
        '''
        Return the list of top-level categories as a list of dictionaries. Each
        dictionary is of the form::
            {
            'name': Display Name,
            'url':URL that gives the JSON object corresponding to all entries in this category,
            'icon': URL to icon of this category,
            'is_category': False for the All Books and Newest categories, True for everything else
            }

        '''
        ans = {}
        categories = self.categories_cache()
        category_meta = self.db.field_metadata

        def getter(x):
            return category_meta[x]['name']

        displayed_custom_fields = custom_fields_to_display(self.db)

        for category in sorted(categories, key=lambda x: sort_key(getter(x))):
            if len(categories[category]) == 0:
                continue
            if category in ('formats', 'identifiers'):
                continue
            meta = category_meta.get(category, None)
            if meta is None:
                continue
            if category_meta.is_ignorable_field(category) and \
                        category not in displayed_custom_fields:
                continue
            display_name = meta['name']
            if category.startswith('@'):
                category = category.partition('.')[0]
                display_name = category[1:]
            url = force_unicode(category)
            icon = category_icon(category, meta)
            ans[url] = (display_name, icon)

        ans = [{
            'url': k,
            'name': v[0],
            'icon': v[1],
            'is_category': True
        } for k, v in ans.iteritems()]
        ans.sort(key=lambda x: sort_key(x['name']))
        for name, url, icon in [
            (_('All books'), 'allbooks', 'book.png'),
            (_('Newest'), 'newest', 'forward.png'),
        ]:
            ans.insert(0, {
                'name': name,
                'url': url,
                'icon': icon,
                'is_category': False
            })

        for c in ans:
            c['url'] = category_url(self.opts.url_prefix, c['url'])
            c['icon'] = icon_url(self.opts.url_prefix, c['icon'])

        return ans

    # }}}

    # Items in the specified category {{{
    @Endpoint()
    def ajax_category(self,
                      name,
                      sort='title',
                      num=100,
                      offset=0,
                      sort_order='asc'):
        '''
        Return a dictionary describing the category specified by name. The
        dictionary looks like::

            {
                'category_name': Category display name,
                'base_url': Base URL for this category,
                'total_num': Total numberof items in this category,
                'offset': The offset for the items returned in this result,
                'num': The number of items returned in this result,
                'sort': How the returned items are sorted,
                'sort_order': asc or desc
                'subcategories': List of sub categories of this category.
                'items': List of items in this category,
            }

        Each subcategory is a dictionary of the same form as those returned by
        ajax_categories().

        Each  item is a dictionary of the form::

            {
                'name': Display name,
                'average_rating': Average rating for books in this item,
                'count': Number of books in this item,
                'url': URL to get list of books in this item,
                'has_children': If True this item contains sub categories, look
                for an entry corresponding to this item in subcategories in the
                main dictionary,
            }

        :param sort: How to sort the returned items. Choices are: name, rating,
                     popularity
        :param sort_order: asc or desc

        To learn how to create subcategories see
        http://manual.calibre-ebook.com/sub_groups.html
        '''
        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r" % num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset)

        base_url = absurl(self.opts.url_prefix, '/ajax/category/' + name)

        if sort not in ('rating', 'name', 'popularity'):
            sort = 'name'

        if sort_order not in ('asc', 'desc'):
            sort_order = 'asc'

        try:
            dname = decode_name(name)
        except:
            raise cherrypy.HTTPError(
                404, 'Invalid encoding of category name'
                ' %r' % name)

        if dname in ('newest', 'allbooks'):
            if dname == 'newest':
                sort, sort_order = 'timestamp', 'desc'
            raise cherrypy.InternalRedirect(
                '/ajax/books_in/%s/%s?sort=%s&sort_order=%s' %
                (encode_name(dname), encode_name('0'), sort, sort_order))

        fm = self.db.field_metadata
        categories = self.categories_cache()
        hierarchical_categories = self.db.prefs['categories_using_hierarchy']

        subcategory = dname
        toplevel = subcategory.partition('.')[0]
        if toplevel == subcategory:
            subcategory = None
        if toplevel not in categories or toplevel not in fm:
            raise cherrypy.HTTPError(404, 'Category %r not found' % toplevel)

        # Find items and sub categories
        subcategories = []
        meta = fm[toplevel]
        item_names = {}
        children = set()

        if meta['kind'] == 'user':
            fullname = ((toplevel + '.' +
                         subcategory) if subcategory is not None else toplevel)
            try:
                # User categories cannot be applied to books, so this is the
                # complete set of items, no need to consider sub categories
                items = categories[fullname]
            except:
                raise cherrypy.HTTPError(
                    404, 'User category %r not found' % fullname)

            parts = fullname.split('.')
            for candidate in categories:
                cparts = candidate.split('.')
                if len(cparts) == len(parts) + 1 and cparts[:-1] == parts:
                    subcategories.append({
                        'name': cparts[-1],
                        'url': candidate,
                        'icon': category_icon(toplevel, meta)
                    })

            category_name = toplevel[1:].split('.')
            # When browsing by user categories we ignore hierarchical normal
            # columns, so children can be empty

        elif toplevel in hierarchical_categories:
            items = []

            category_names = [
                x.original_name.split('.') for x in categories[toplevel]
                if '.' in x.original_name
            ]

            if subcategory is None:
                children = set(x[0] for x in category_names)
                category_name = [meta['name']]
                items = [
                    x for x in categories[toplevel]
                    if '.' not in x.original_name
                ]
            else:
                subcategory_parts = subcategory.split('.')[1:]
                category_name = [meta['name']] + subcategory_parts

                lsp = len(subcategory_parts)
                children = set(
                    '.'.join(x) for x in category_names
                    if len(x) == lsp + 1 and x[:lsp] == subcategory_parts)
                items = [
                    x for x in categories[toplevel]
                    if x.original_name in children
                ]
                item_names = {
                    x: x.original_name.rpartition('.')[-1]
                    for x in items
                }
                # Only mark the subcategories that have children themselves as
                # subcategories
                children = set(
                    '.'.join(x[:lsp + 1]) for x in category_names
                    if len(x) > lsp + 1 and x[:lsp] == subcategory_parts)
            subcategories = [{
                'name': x.rpartition('.')[-1],
                'url': toplevel + '.' + x,
                'icon': category_icon(toplevel, meta)
            } for x in children]
        else:
            items = categories[toplevel]
            category_name = meta['name']

        for x in subcategories:
            x['url'] = category_url(self.opts.url_prefix, x['url'])
            x['icon'] = icon_url(self.opts.url_prefix, x['icon'])
            x['is_category'] = True

        sort_keygen = {
            'name': lambda x: sort_key(x.sort if x.sort else x.original_name),
            'popularity': lambda x: x.count,
            'rating': lambda x: x.avg_rating
        }
        items.sort(key=sort_keygen[sort], reverse=sort_order == 'desc')
        total_num = len(items)
        items = items[offset:offset + num]
        items = [{
            'name':
            item_names.get(x, x.original_name),
            'average_rating':
            x.avg_rating,
            'count':
            x.count,
            'url':
            books_in_url(self.opts.url_prefix,
                         x.category if x.category else toplevel,
                         x.original_name if x.id is None else unicode(x.id)),
            'has_children':
            x.original_name in children,
        } for x in items]

        return {
            'category_name': category_name,
            'base_url': base_url,
            'total_num': total_num,
            'offset': offset,
            'num': len(items),
            'sort': sort,
            'sort_order': sort_order,
            'subcategories': subcategories,
            'items': items,
        }

    # }}}

    # Books in the specified category {{{
    @Endpoint()
    def ajax_books_in(self,
                      category,
                      item,
                      sort='title',
                      num=25,
                      offset=0,
                      sort_order='asc',
                      get_additional_fields=''):
        '''
        Return the books (as list of ids) present in the specified category.
        '''
        try:
            dname, ditem = map(decode_name, (category, item))
        except:
            raise cherrypy.HTTPError(404,
                                     'Invalid encoded param: %r' % category)

        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r" % num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset)

        if sort_order not in ('asc', 'desc'):
            sort_order = 'asc'

        sfield = self.db.data.sanitize_sort_field_name(sort)
        if sfield not in self.db.field_metadata.sortable_field_keys():
            raise cherrypy.HTTPError(404,
                                     '%s is not a valid sort field' % sort)

        if dname in ('allbooks', 'newest'):
            ids = self.search_cache('')
        elif dname == 'search':
            try:
                ids = self.search_cache('search:"%s"' % ditem)
            except:
                raise cherrypy.HTTPError(404,
                                         'Search: %r not understood' % ditem)
        else:
            try:
                cid = int(ditem)
            except:
                raise cherrypy.HTTPError(
                    404, 'Category id %r not an integer' % ditem)

            if dname == 'news':
                dname = 'tags'
            ids = self.db.get_books_for_category(dname, cid)
            all_ids = set(self.search_cache(''))
            # Implement restriction
            ids = ids.intersection(all_ids)

        ids = list(ids)
        self.db.data.multisort(fields=[(sfield, sort_order == 'asc')],
                               subsort=True,
                               only_ids=ids)
        total_num = len(ids)
        ids = ids[offset:offset + num]

        result = {
            'total_num':
            total_num,
            'sort_order':
            sort_order,
            'offset':
            offset,
            'num':
            len(ids),
            'sort':
            sort,
            'base_url':
            absurl(self.opts.url_prefix,
                   '/ajax/books_in/%s/%s' % (category, item)),
            'book_ids':
            ids
        }

        if get_additional_fields:
            additional_fields = {}
            for field in get_additional_fields.split(','):
                field = field.strip()
                if field:
                    flist = additional_fields[field] = []
                    for id_ in ids:
                        flist.append(
                            self.db.new_api.field_for(field,
                                                      id_,
                                                      default_value=None))
            if additional_fields:
                result['additional_fields'] = additional_fields
        return result

    # }}}

    # Search {{{
    @Endpoint()
    def ajax_search(self,
                    query='',
                    sort='title',
                    offset=0,
                    num=25,
                    sort_order='asc'):
        '''
        Return the books (as list of ids) matching the specified search query.
        '''

        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r" % num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r" % offset)
        sfield = self.db.data.sanitize_sort_field_name(sort)
        if sfield not in self.db.field_metadata.sortable_field_keys():
            raise cherrypy.HTTPError(404,
                                     '%s is not a valid sort field' % sort)

        if isbytestring(query):
            query = query.decode('UTF-8')
        ids = list(self.search_for_books(query))
        self.db.data.multisort(fields=[(sfield, sort_order == 'asc')],
                               subsort=True,
                               only_ids=ids)
        total_num = len(ids)
        ids = ids[offset:offset + num]
        return {
            'total_num': total_num,
            'sort_order': sort_order,
            'offset': offset,
            'num': len(ids),
            'sort': sort,
            'base_url': absurl(self.opts.url_prefix, '/ajax/search'),
            'query': query,
            'book_ids': ids
        }
Exemplo n.º 8
0
def book_to_json(ctx, rd, db, book_id,
                 get_category_urls=True, device_compatible=False, device_for_template=None):
    mi = db.get_metadata(book_id, get_cover=False)
    codec = JsonCodec(db.field_metadata)
    if not device_compatible:
        try:
            mi.rating = mi.rating/2.
        except Exception:
            mi.rating = 0.0
    data = codec.encode_book_metadata(mi)
    for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime',
            'rights', 'book_producer'):
        data.pop(x, None)

    get = partial(ctx.url_for, get_content, book_id=book_id, library_id=db.server_library_id)
    data['cover'] = get(what='cover')
    data['thumbnail'] = get(what='thumb')

    if not device_compatible:
        mi.format_metadata = {k.lower():dict(v) for k, v in
                mi.format_metadata.iteritems()}
        for v in mi.format_metadata.itervalues():
            mtime = v.get('mtime', None)
            if mtime is not None:
                v['mtime'] = isoformat(mtime, as_utc=True)
        data['format_metadata'] = mi.format_metadata
        fmts = set(x.lower() for x in mi.format_metadata.iterkeys())
        pf = prefs['output_format'].lower()
        other_fmts = list(fmts)
        try:
            fmt = pf if pf in fmts else other_fmts[0]
        except:
            fmt = None
        if fmts and fmt:
            other_fmts = [x for x in fmts if x != fmt]
        data['formats'] = sorted(fmts)
        if fmt:
            data['main_format'] = {fmt:get(what=fmt)}
        else:
            data['main_format'] = None
        data['other_formats'] = {fmt:get(what=fmt) for fmt in other_fmts}

        if get_category_urls:
            category_urls = data['category_urls'] = {}
            all_cats = ctx.get_categories(rd, db)
            for key in mi.all_field_keys():
                fm = mi.metadata_for_field(key)
                if (fm and fm['is_category'] and not fm['is_csp'] and
                        key != 'formats' and fm['datatype'] != 'rating'):
                    categories = mi.get(key) or []
                    if isinstance(categories, basestring):
                        categories = [categories]
                    category_urls[key] = dbtags = {}
                    for category in categories:
                        for tag in all_cats.get(key, ()):
                            if tag.original_name == category:
                                dbtags[category] = ctx.url_for(
                                    books_in,
                                    encoded_category=encode_name(tag.category if tag.category else key),
                                    encoded_item=encode_name(tag.original_name if tag.id is None else unicode(tag.id)),
                                    library_id=db.server_library_id
                                )
                                break
    else:
        series = data.get('series', None) or ''
        if series:
            tsorder = tweaks['save_template_title_series_sorting']
            series = title_sort(series, order=tsorder)
        data['_series_sort_'] = series
        if device_for_template:
            import posixpath
            from calibre.devices.utils import create_upload_path
            from calibre.utils.filenames import ascii_filename as sanitize
            from calibre.customize.ui import device_plugins

            for device_class in device_plugins():
                if device_class.__class__.__name__ == device_for_template:
                    template = device_class.save_template()
                    data['_filename_'] = create_upload_path(mi, book_id,
                            template, sanitize, path_type=posixpath)
                    break

    return data, mi.last_modified
Exemplo n.º 9
0
 def __init__(self):
     self.ajax_json_codec = JsonCodec()
Exemplo n.º 10
0
class AjaxServer(object):

    def __init__(self):
        self.ajax_json_codec = JsonCodec()

    def add_routes(self, connect):
        base_href = '/ajax'

        # Metadata for books
        connect('ajax_book', base_href+'/book/{book_id}', self.ajax_book)
        connect('ajax_books', base_href+'/books', self.ajax_books)

        # The list of top level categories
        connect('ajax_categories', base_href+'/categories',
                self.ajax_categories)

        # The list of sub-categories and items in each category
        connect('ajax_category', base_href+'/category/{name}',
                self.ajax_category)

        # List of books in specified category
        connect('ajax_books_in', base_href+'/books_in/{category}/{item}',
                self.ajax_books_in)

        # Search
        connect('ajax_search', base_href+'/search', self.ajax_search)

    # Get book metadata {{{
    def ajax_book_to_json(self, book_id, get_category_urls=True,
                          device_compatible=False, device_for_template=None):
        mi = self.db.get_metadata(book_id, index_is_id=True)

        if not device_compatible:
            try:
                mi.rating = mi.rating/2.
            except:
                mi.rating = 0.0

        data = self.ajax_json_codec.encode_book_metadata(mi)
        for x in ('publication_type', 'size', 'db_id', 'lpath', 'mime',
                'rights', 'book_producer'):
            data.pop(x, None)

        data['cover'] = absurl(self.opts.url_prefix, u'/get/cover/%d'%book_id)
        data['thumbnail'] = absurl(self.opts.url_prefix, u'/get/thumb/%d'%book_id)

        if not device_compatible:
            mi.format_metadata = {k.lower():dict(v) for k, v in
                    mi.format_metadata.iteritems()}
            for v in mi.format_metadata.itervalues():
                mtime = v.get('mtime', None)
                if mtime is not None:
                    v['mtime'] = isoformat(mtime, as_utc=True)
            data['format_metadata'] = mi.format_metadata
            fmts = set(x.lower() for x in mi.format_metadata.iterkeys())
            pf = prefs['output_format'].lower()
            other_fmts = list(fmts)
            try:
                fmt = pf if pf in fmts else other_fmts[0]
            except:
                fmt = None
            if fmts and fmt:
                other_fmts = [x for x in fmts if x != fmt]
            data['formats'] = sorted(fmts)
            if fmt:
                data['main_format'] = {fmt: absurl(self.opts.url_prefix, u'/get/%s/%d'%(fmt, book_id))}
            else:
                data['main_format'] = None
            data['other_formats'] = {fmt: absurl(self.opts.url_prefix, u'/get/%s/%d'%(fmt, book_id)) for fmt
                    in other_fmts}

            if get_category_urls:
                category_urls = data['category_urls'] = {}
                ccache = self.categories_cache()
                for key in mi.all_field_keys():
                    fm = mi.metadata_for_field(key)
                    if (fm and fm['is_category'] and not fm['is_csp'] and
                            key != 'formats' and fm['datatype'] not in ['rating']):
                        categories = mi.get(key)
                        if isinstance(categories, basestring):
                            categories = [categories]
                        if categories is None:
                            categories = []
                        dbtags = {}
                        for category in categories:
                            for tag in ccache.get(key, []):
                                if tag.original_name == category:
                                    dbtags[category] = books_in_url(self.opts.url_prefix,
                                        tag.category if tag.category else key,
                                        tag.original_name if tag.id is None else
                                        unicode(tag.id))
                                    break
                        category_urls[key] = dbtags
        else:
            series = data.get('series', None)
            if series:
                tsorder = tweaks['save_template_title_series_sorting']
                series = title_sort(series, order=tsorder)
            else:
                series = ''
            data['_series_sort_'] = series
            if device_for_template:
                import posixpath
                from calibre.devices.utils import create_upload_path
                from calibre.utils.filenames import ascii_filename as sanitize
                from calibre.customize.ui import device_plugins

                for device_class in device_plugins():
                    if device_class.__class__.__name__ == device_for_template:
                        template = device_class.save_template()
                        data['_filename_'] = create_upload_path(mi, book_id,
                                template, sanitize, path_type=posixpath)
                        break

        return data, mi.last_modified

    @Endpoint(set_last_modified=False)
    def ajax_book(self, book_id, category_urls='true', id_is_uuid='false',
                  device_compatible='false', device_for_template=None):
        '''
        Return the metadata of the book as a JSON dictionary.

        If category_urls == 'true' the returned dictionary also contains a
        mapping of category names to URLs that return the list of books in the
        given category.
        '''
        cherrypy.response.timeout = 3600

        try:
            if id_is_uuid == 'true':
                book_id = self.db.get_id_from_uuid(book_id)
            else:
                book_id = int(book_id)
            data, last_modified = self.ajax_book_to_json(book_id,
                    get_category_urls=category_urls.lower()=='true',
                    device_compatible=device_compatible.lower()=='true',
                    device_for_template=device_for_template)
        except:
            raise cherrypy.HTTPError(404, 'No book with id: %r'%book_id)

        cherrypy.response.headers['Last-Modified'] = \
                self.last_modified(last_modified)

        return data

    @Endpoint(set_last_modified=False)
    def ajax_books(self, ids=None, category_urls='true', id_is_uuid='false', device_for_template=None):
        '''
        Return the metadata for a list of books specified as a comma separated
        list of ids. The metadata is returned as a dictionary mapping ids to
        the metadata. The format for the metadata is the same as in
        ajax_book(). If no book is found for a given id, it is mapped to null
        in the dictionary.

        This endpoint can be used with either GET or POST requests, variable
        name is ids: /ajax/books?ids=1,2,3,4,5
        '''
        if ids is None:
            raise cherrypy.HTTPError(404, 'Must specify some ids')
        try:
            if id_is_uuid == 'true':
                ids = set(self.db.get_id_from_uuid(x) for x in ids.split(','))
            else:
                ids = set(int(x.strip()) for x in ids.split(','))
        except:
            raise cherrypy.HTTPError(404, 'ids must be a comma separated list'
                    ' of integers')
        ans = {}
        lm = None
        gcu = category_urls.lower()=='true'
        for book_id in ids:
            try:
                data, last_modified = self.ajax_book_to_json(book_id,
                        get_category_urls=gcu, device_for_template=device_for_template)
            except:
                ans[book_id] = None
            else:
                ans[book_id] = data
                if lm is None or last_modified > lm:
                    lm = last_modified

        cherrypy.response.timeout = 3600
        cherrypy.response.headers['Last-Modified'] = \
                self.last_modified(lm if lm is not None else
                        self.db.last_modified())

        return ans

    # }}}

    # Top level categories {{{
    @Endpoint()
    def ajax_categories(self):
        '''
        Return the list of top-level categories as a list of dictionaries. Each
        dictionary is of the form::
            {
            'name': Display Name,
            'url':URL that gives the JSON object corresponding to all entries in this category,
            'icon': URL to icon of this category,
            'is_category': False for the All Books and Newest categories, True for everything else
            }

        '''
        ans = {}
        categories = self.categories_cache()
        category_meta = self.db.field_metadata

        def getter(x):
            return category_meta[x]['name']

        displayed_custom_fields = custom_fields_to_display(self.db)

        for category in sorted(categories, key=lambda x: sort_key(getter(x))):
            if len(categories[category]) == 0:
                continue
            if category in ('formats', 'identifiers'):
                continue
            meta = category_meta.get(category, None)
            if meta is None:
                continue
            if category_meta.is_ignorable_field(category) and \
                        category not in displayed_custom_fields:
                continue
            display_name = meta['name']
            if category.startswith('@'):
                category = category.partition('.')[0]
                display_name = category[1:]
            url = force_unicode(category)
            icon = category_icon(category, meta)
            ans[url] = (display_name, icon)

        ans = [{'url':k, 'name':v[0], 'icon':v[1], 'is_category':True}
                for k, v in ans.iteritems()]
        ans.sort(key=lambda x: sort_key(x['name']))
        for name, url, icon in [
                (_('All books'), 'allbooks', 'book.png'),
                (_('Newest'), 'newest', 'forward.png'),
                ]:
            ans.insert(0, {'name':name, 'url':url, 'icon':icon,
                'is_category':False})

        for c in ans:
            c['url'] = category_url(self.opts.url_prefix, c['url'])
            c['icon'] = icon_url(self.opts.url_prefix, c['icon'])

        return ans
    # }}}

    # Items in the specified category {{{
    @Endpoint()
    def ajax_category(self, name, sort='title', num=100, offset=0,
            sort_order='asc'):
        '''
        Return a dictionary describing the category specified by name. The
        dictionary looks like::

            {
                'category_name': Category display name,
                'base_url': Base URL for this category,
                'total_num': Total numberof items in this category,
                'offset': The offset for the items returned in this result,
                'num': The number of items returned in this result,
                'sort': How the returned items are sorted,
                'sort_order': asc or desc
                'subcategories': List of sub categories of this category.
                'items': List of items in this category,
            }

        Each subcategory is a dictionary of the same form as those returned by
        ajax_categories().

        Each  item is a dictionary of the form::

            {
                'name': Display name,
                'average_rating': Average rating for books in this item,
                'count': Number of books in this item,
                'url': URL to get list of books in this item,
                'has_children': If True this item contains sub categories, look
                for an entry corresponding to this item in subcategories in the
                main dictionary,
            }

        :param sort: How to sort the returned items. Choices are: name, rating,
                     popularity
        :param sort_order: asc or desc

        To learn how to create subcategories see
        http://manual.calibre-ebook.com/sub_groups.html
        '''
        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r"%num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset)

        base_url = absurl(self.opts.url_prefix, '/ajax/category/'+name)

        if sort not in ('rating', 'name', 'popularity'):
            sort = 'name'

        if sort_order not in ('asc', 'desc'):
            sort_order = 'asc'

        try:
            dname = decode_name(name)
        except:
            raise cherrypy.HTTPError(404, 'Invalid encoding of category name'
                    ' %r'%name)

        if dname in ('newest', 'allbooks'):
            if dname == 'newest':
                sort, sort_order = 'timestamp', 'desc'
            raise cherrypy.InternalRedirect(
                '/ajax/books_in/%s/%s?sort=%s&sort_order=%s'%(
                    encode_name(dname), encode_name('0'), sort, sort_order))

        fm = self.db.field_metadata
        categories = self.categories_cache()
        hierarchical_categories = self.db.prefs['categories_using_hierarchy']

        subcategory = dname
        toplevel = subcategory.partition('.')[0]
        if toplevel == subcategory:
            subcategory = None
        if toplevel not in categories or toplevel not in fm:
            raise cherrypy.HTTPError(404, 'Category %r not found'%toplevel)

        # Find items and sub categories
        subcategories = []
        meta = fm[toplevel]
        item_names = {}
        children = set()

        if meta['kind'] == 'user':
            fullname = ((toplevel + '.' + subcategory) if subcategory is not
                                None else toplevel)
            try:
                # User categories cannot be applied to books, so this is the
                # complete set of items, no need to consider sub categories
                items = categories[fullname]
            except:
                raise cherrypy.HTTPError(404,
                        'User category %r not found'%fullname)

            parts = fullname.split('.')
            for candidate in categories:
                cparts = candidate.split('.')
                if len(cparts) == len(parts)+1 and cparts[:-1] == parts:
                    subcategories.append({'name':cparts[-1],
                        'url':candidate,
                        'icon':category_icon(toplevel, meta)})

            category_name = toplevel[1:].split('.')
            # When browsing by user categories we ignore hierarchical normal
            # columns, so children can be empty

        elif toplevel in hierarchical_categories:
            items = []

            category_names = [x.original_name.split('.') for x in categories[toplevel] if
                    '.' in x.original_name]

            if subcategory is None:
                children = set(x[0] for x in category_names)
                category_name = [meta['name']]
                items = [x for x in categories[toplevel] if '.' not in x.original_name]
            else:
                subcategory_parts = subcategory.split('.')[1:]
                category_name = [meta['name']] + subcategory_parts

                lsp = len(subcategory_parts)
                children = set('.'.join(x) for x in category_names if len(x) ==
                        lsp+1 and x[:lsp] == subcategory_parts)
                items = [x for x in categories[toplevel] if x.original_name in
                        children]
                item_names = {x:x.original_name.rpartition('.')[-1] for x in
                        items}
                # Only mark the subcategories that have children themselves as
                # subcategories
                children = set('.'.join(x[:lsp+1]) for x in category_names if len(x) >
                        lsp+1 and x[:lsp] == subcategory_parts)
            subcategories = [{'name':x.rpartition('.')[-1],
                'url':toplevel+'.'+x,
                'icon':category_icon(toplevel, meta)} for x in children]
        else:
            items = categories[toplevel]
            category_name = meta['name']

        for x in subcategories:
            x['url'] = category_url(self.opts.url_prefix, x['url'])
            x['icon'] = icon_url(self.opts.url_prefix, x['icon'])
            x['is_category'] = True

        sort_keygen = {
                'name': lambda x: sort_key(x.sort if x.sort else x.original_name),
                'popularity': lambda x: x.count,
                'rating': lambda x: x.avg_rating
        }
        items.sort(key=sort_keygen[sort], reverse=sort_order == 'desc')
        total_num = len(items)
        items = items[offset:offset+num]
        items = [{
            'name':item_names.get(x, x.original_name),
            'average_rating': x.avg_rating,
            'count': x.count,
            'url': books_in_url(self.opts.url_prefix,
                x.category if x.category else toplevel,
                x.original_name if x.id is None else unicode(x.id)),
            'has_children': x.original_name in children,
            } for x in items]

        return {
                'category_name': category_name,
                'base_url': base_url,
                'total_num': total_num,
                'offset':offset, 'num':len(items), 'sort':sort,
                'sort_order':sort_order,
                'subcategories':subcategories,
                'items':items,
        }

    # }}}

    # Books in the specified category {{{
    @Endpoint()
    def ajax_books_in(self, category, item, sort='title', num=25, offset=0,
            sort_order='asc', get_additional_fields=''):
        '''
        Return the books (as list of ids) present in the specified category.
        '''
        try:
            dname, ditem = map(decode_name, (category, item))
        except:
            raise cherrypy.HTTPError(404, 'Invalid encoded param: %r'%category)

        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r"%num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset)

        if sort_order not in ('asc', 'desc'):
            sort_order = 'asc'

        sfield = self.db.data.sanitize_sort_field_name(sort)
        if sfield not in self.db.field_metadata.sortable_field_keys():
            raise cherrypy.HTTPError(404, '%s is not a valid sort field'%sort)

        if dname in ('allbooks', 'newest'):
            ids = self.search_cache('')
        elif dname == 'search':
            try:
                ids = self.search_cache('search:"%s"'%ditem)
            except:
                raise cherrypy.HTTPError(404, 'Search: %r not understood'%ditem)
        else:
            try:
                cid = int(ditem)
            except:
                raise cherrypy.HTTPError(404,
                        'Category id %r not an integer'%ditem)

            if dname == 'news':
                dname = 'tags'
            ids = self.db.get_books_for_category(dname, cid)
            all_ids = set(self.search_cache(''))
            # Implement restriction
            ids = ids.intersection(all_ids)

        ids = list(ids)
        self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True,
                only_ids=ids)
        total_num = len(ids)
        ids = ids[offset:offset+num]

        result = {
                'total_num': total_num, 'sort_order':sort_order,
                'offset':offset, 'num':len(ids), 'sort':sort,
                'base_url':absurl(self.opts.url_prefix, '/ajax/books_in/%s/%s'%(category, item)),
                'book_ids':ids
        }

        if get_additional_fields:
            additional_fields = {}
            for field in get_additional_fields.split(','):
                field = field.strip()
                if field:
                    flist = additional_fields[field] = []
                    for id_ in ids:
                        flist.append(self.db.new_api.field_for(field, id_,
                                                               default_value=None))
            if additional_fields:
                result['additional_fields'] = additional_fields
        return result

    # }}}

    # Search {{{
    @Endpoint()
    def ajax_search(self, query='', sort='title', offset=0, num=25,
            sort_order='asc'):
        '''
        Return the books (as list of ids) matching the specified search query.
        '''

        try:
            num = int(num)
        except:
            raise cherrypy.HTTPError(404, "Invalid num: %r"%num)
        try:
            offset = int(offset)
        except:
            raise cherrypy.HTTPError(404, "Invalid offset: %r"%offset)
        sfield = self.db.data.sanitize_sort_field_name(sort)
        if sfield not in self.db.field_metadata.sortable_field_keys():
            raise cherrypy.HTTPError(404, '%s is not a valid sort field'%sort)

        if isbytestring(query):
            query = query.decode('UTF-8')
        ids = list(self.search_for_books(query))
        self.db.data.multisort(fields=[(sfield, sort_order == 'asc')], subsort=True,
                only_ids=ids)
        total_num = len(ids)
        ids = ids[offset:offset+num]
        return {
                'total_num': total_num, 'sort_order':sort_order,
                'offset':offset, 'num':len(ids), 'sort':sort,
                'base_url':absurl(self.opts.url_prefix, '/ajax/search'),
                'query': query,
                'book_ids':ids
        }
Exemplo n.º 11
0
class BrowseServer(object):

    def __init__(self):
        self.json_codec = JsonCodec()

    def add_routes(self, connect):
        base_href = '/browse'
        connect('browse', base_href, self.browse_catalog)
        connect('browse_catalog', base_href+'/category/{category}',
                self.browse_catalog)
        connect('browse_category_group',
                base_href+'/category_group/{category}/{group}',
                self.browse_category_group)
        connect('browse_matches',
                base_href+'/matches/{category}/{cid}',
                self.browse_matches)
        connect('browse_booklist_page',
                base_href+'/booklist_page',
                self.browse_booklist_page)
        connect('browse_search', base_href+'/search',
                self.browse_search)
        connect('browse_details', base_href+'/details/{id}',
                self.browse_details)
        connect('browse_book', base_href+'/book/{id}',
                self.browse_book)
        connect('browse_random', base_href+'/random',
                self.browse_random)
        connect('browse_category_icon', base_href+'/icon/{name}',
                self.browse_icon)

        self.icon_map = JSONConfig('gui').get('tags_browser_category_icons', {})
        self.nav = self.browse_nav()

    # Templates {{{
    def browse_template(self, sort, category=True, initial_search=''):

        if not hasattr(self, '__browse_template__') or \
                self.opts.develop:
            self.__browse_template__ = \
                P('content_server/browse/browse.html', data=True).decode('utf-8')

        ans = self.__browse_template__
        scn = 'calibre_browse_server_sort_'

        if category:
            sort_opts = [('rating', _('Average rating')), ('name',
                _('Name')), ('popularity', _('Popularity'))]
            scn += 'category'
        else:
            scn += 'list'
            fm = self.db.field_metadata
            sort_opts, added = [], set([])
            displayed_custom_fields = custom_fields_to_display(self.db)
            for x in fm.sortable_field_keys():
                if x in ('ondevice', 'formats', 'sort'):
                    continue
                if fm.is_ignorable_field(x) and x not in displayed_custom_fields:
                    continue
                if x == 'comments' or fm[x]['datatype'] == 'comments':
                    continue
                n = fm[x]['name']
                if n not in added:
                    added.add(n)
                    sort_opts.append((x, n))

        ans = ans.replace('{sort_select_label}', xml(_('Sort by')+':'))
        ans = ans.replace('{sort_cookie_name}', scn)
        ans = ans.replace('{prefix}', self.opts.url_prefix)
        ans = ans.replace('{library}', _('library'))
        ans = ans.replace('{home}', _('home'))
        ans = ans.replace('{Search}', _('Search'))
        ans = ans.replace('{nav}', self.nav)
        opts = ['<option %svalue="%s">%s</option>' % (
            'selected="selected" ' if k==sort else '',
            xml(k), xml(nl), ) for k, nl in
                sorted(sort_opts, key=lambda x: sort_key(operator.itemgetter(1)(x))) if k and nl]
        ans = ans.replace('{sort_select_options}', ('\n'+' '*20).join(opts))
        lp = self.db.library_path
        if isbytestring(lp):
            lp = force_unicode(lp, filesystem_encoding)
        ans = ans.replace('{library_name}', xml(os.path.basename(lp)))
        ans = ans.replace('{library_path}', xml(lp, True))
        ans = ans.replace('{initial_search}', xml(initial_search, attribute=True))
        return ans

    @property
    def browse_summary_template(self):
        if not hasattr(self, '__browse_summary_template__') or \
                self.opts.develop:
            self.__browse_summary_template__ = \
                P('content_server/browse/summary.html', data=True).decode('utf-8')
        return self.__browse_summary_template__.replace('{prefix}',
                self.opts.url_prefix)

    @property
    def browse_details_template(self):
        if not hasattr(self, '__browse_details_template__') or \
                self.opts.develop:
            self.__browse_details_template__ = \
                P('content_server/browse/details.html', data=True).decode('utf-8')
        return self.__browse_details_template__.replace('{prefix}',
                self.opts.url_prefix)

    # }}}

    # Catalogs {{{
    def browse_icon(self, name='blank.png'):
        cherrypy.response.headers['Content-Type'] = 'image/png'
        cherrypy.response.headers['Last-Modified'] = self.last_modified(self.build_time)

        if not hasattr(self, '__browse_icon_cache__'):
            self.__browse_icon_cache__ = {}
        if name not in self.__browse_icon_cache__:
            if name.startswith('_'):
                name = sanitize_file_name2(name[1:])
                try:
                    with open(os.path.join(config_dir, 'tb_icons', name), 'rb') as f:
                        data = f.read()
                except:
                    raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
            else:
                try:
                    data = I(name, data=True)
                except:
                    raise cherrypy.HTTPError(404, 'no icon named: %r'%name)
            img = Image()
            img.load(data)
            width, height = img.size
            scaled, width, height = fit_image(width, height, 48, 48)
            if scaled:
                img.size = (width, height)

            self.__browse_icon_cache__[name] = img.export('png')
        return self.__browse_icon_cache__[name]

    def browse_nav(self):
        categories = self.categories_cache()
        category_meta = self.db.field_metadata
        cats = [
                (_('Newest'), 'newest', 'whatshot'),
                (_('All books'), 'allbooks', 'library_books'),
                (_('Random book'), 'randombook', 'shuffle'),
                ]
        virt_libs = self.db.prefs.get('virtual_libraries', {})
        if virt_libs:
            cats.append((_('Virtual Libs.'), 'virt_libs', 'graphic_eq'))

        def getter(x):
            try:
                return category_meta[x]['name'].lower()
            except KeyError:
                return x

        displayed_custom_fields = custom_fields_to_display(self.db)
        uc_displayed = set()
        for category in sorted(categories, key=lambda x: sort_key(getter(x))):
            if len(categories[category]) == 0:
                continue
            if category in ('formats', 'identifiers'):
                continue
            meta = category_meta.get(category, None)
            if meta is None:
                continue
            if self.db.field_metadata.is_ignorable_field(category) and \
                        category not in displayed_custom_fields:
                continue

            # get the icon files
            main_cat = (category.partition('.')[0]) if hasattr(category,
                                                    'partition') else category
            if main_cat in self.icon_map:
                icon = self.icon_map[main_cat]
            elif category in category_icon_map:
                icon = category_icon_map[category]
            elif meta['is_custom']:
                icon = category_icon_map['custom:']
            elif meta['kind'] == 'user':
                icon = category_icon_map['user:'******'check_box_outline_blank'

            if meta['kind'] == 'user':
                dot = category.find('.')
                if dot > 0:
                    cat = category[:dot]
                    if cat not in uc_displayed:
                        cats.append((meta['name'][:dot-1], cat, icon))
                        uc_displayed.add(cat)
                else:
                    cats.append((meta['name'], category, icon))
                    uc_displayed.add(category)
            else:
                cats.append((meta['name'], category, icon))

        cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">'
                 u'<i class="material-icons">{icon}</i>'
                 u'<span>{0}</span></a>'
                 u'</li>')
                .format(xml(name, True), xml(quote(cat)), xml(_('Browse books by')),
                    self.opts.url_prefix, icon=icon)
                for name, cat, icon in cats]

        return '\n'.join(cats)

    def books_to_json(self, *books):
        result = []
        for book in books:
            book_json = self.json_codec.encode_book_metadata(book)
            book_json['formats'] = [fmt.lower() for fmt in book.formats]
            book_json['format_metadata'] = {k.lower(): dict(v) for k, v in book.format_metadata.iteritems()}
            for v in book_json['format_metadata'].itervalues():
                v.pop('mtime', None)
            result.append(book_json)
        return json.dumps(result, skipkeys=True)

    def browse_json_books(self, ids, prefix, suffix=''):
        books = [self.db.get_metadata(id, True) for id in ids]
        books_json = self.books_to_json(*books)
        return "<script>window.calibre_books = {}</script>".format(books_json)

    def browse_toplevel(self):
        main = u'<div class="toplevel"></div>'
        return self.browse_template('name').format(title='', main=main)

    def browse_sort_categories(self, items, sort):
        if sort not in ('rating', 'name', 'popularity'):
            sort = 'name'
        items.sort(key=lambda x: sort_key(getattr(x, 'sort', x.name)))
        if sort == 'popularity':
            items.sort(key=operator.attrgetter('count'), reverse=True)
        elif sort == 'rating':
            items.sort(key=operator.attrgetter('avg_rating'), reverse=True)
        return sort

    def browse_category(self, category, sort):
        categories = self.categories_cache()
        categories['virt_libs'] = {}
        if category not in categories:
            raise cherrypy.HTTPError(404, 'category not found')
        category_meta = self.db.field_metadata
        category_name = _('Virtual Libraries') if category == 'virt_libs' else category_meta[category]['name']
        datatype = 'text' if category == 'virt_libs' else category_meta[category]['datatype']

        # See if we have any sub-categories to display. As we find them, add
        # them to the displayed set to avoid showing the same item twice
        uc_displayed = set()
        cats = []
        for ucat in sorted(categories.keys(), key=sort_key):
            if len(categories[ucat]) == 0:
                continue
            if category == 'formats':
                continue
            meta = category_meta.get(ucat, None)
            if meta is None:
                continue
            if meta['kind'] != 'user':
                continue
            cat_len = len(category)
            if not (len(ucat) > cat_len and ucat.startswith(category+'.')):
                continue

            if ucat in self.icon_map:
                icon = '_'+quote(self.icon_map[ucat])
            else:
                icon = category_icon_map['user:'******'.')
            if dot > 0:
                # More subcats
                cat = cat[:dot]
                if cat not in uc_displayed:
                    cats.append((cat, ucat[:cat_len+dot], icon))
                    uc_displayed.add(cat)
            else:
                # This is the end of the chain
                cats.append((cat, ucat, icon))
                uc_displayed.add(cat)

        cats = u'\n\n'.join(
                [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}">&nbsp;</a>'
                 u'<img src="{3}{src}" alt="{0}" />'
                 u'<span class="label">{0}</span>'
                 u'</li>')
                .format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
                    self.opts.url_prefix, src='/browse/icon/'+z)
                for x, y, z in cats])
        if cats:
            cats = (u'\n<div class="toplevel">\n'
                     '{0}</div>').format(cats)

        # Now do the category items
        vls = self.db.prefs.get('virtual_libraries', {})
        categories['virt_libs'] = sorted([Tag(k) for k, v in vls.iteritems()], key=lambda x:sort_key(x.name))
        items = categories[category]
        print(items)

        sort = self.browse_sort_categories(items, sort)

        if not cats and len(items) == 1:
            # Only one item in category, go directly to book list
            html = get_category_items(category, items,
                    datatype, self.opts.url_prefix)
            href = re.search(r'<a href="([^"]+)"', html)
            if href is not None:
                # cherrypy does not auto unquote params when using
                # InternalRedirect
                raise cherrypy.InternalRedirect(unquote(href.group(1)))

        if len(items) <= self.opts.max_opds_ungrouped_items:
            items = get_category_items(category, items,
                    datatype, self.opts.url_prefix)
        else:
            getter = lambda x: unicode(getattr(x, 'sort', None) or x.name)
            starts = set([])
            for x in items:
                val = getter(x)
                if not val:
                    val = u'A'
                starts.add(val[0].upper())
            category_groups = OrderedDict()
            for x in sorted(starts):
                category_groups[x] = len([y for y in items if
                    getter(y).upper().startswith(x)])
            items = [(u'<h3 title="{0}"><a class="load_href" title="{0}"'
                      u' href="{4}{3}"><strong>{0}</strong> [{2}]</a></h3><div>'
                      u'<div class="loaded" style="display:none"></div>'
                      u'<div class="loading"><img alt="{1}" src="{4}/static/loading.gif" /><em>{1}</em></div>'
                      u'</div>').format(
                        xml(s, True),
                        xml(_('Loading, please wait'))+'&hellip;',
                        unicode(c),
                        xml(u'/browse/category_group/%s/%s'%(
                            hexlify(category.encode('utf-8')),
                            hexlify(s.encode('utf-8'))), True),
                        self.opts.url_prefix)
                    for s, c in category_groups.items()]
            items = '\n\n'.join(items)
            items = u'<div id="groups">\n{0}</div>'.format(items)

        main = u'''
            <div class="category">
                <h3>{0}</h3>
                    <a class="navlink" href="{3}/browse"
                        title="{2}">{2}&nbsp;&uarr;</a>
                {1}
            </div>
        '''.format(
                xml(_('Browsing by')+': ' + category_name), cats + items,
                xml(_('Up'), True), self.opts.url_prefix)

        return self.browse_template(sort).format(title=category_name,
                main=main)

    @Endpoint(mimetype='application/json; charset=utf-8')
    def browse_category_group(self, category=None, group=None, sort=None):
        if sort == 'null':
            sort = None
        if sort not in ('rating', 'name', 'popularity'):
            sort = 'name'
        try:
            category = unhexlify(category)
            if isbytestring(category):
                category = category.decode('utf-8')
        except:
            raise cherrypy.HTTPError(404, 'invalid category')

        categories = self.categories_cache()
        if category not in categories:
            raise cherrypy.HTTPError(404, 'category not found')

        category_meta = self.db.field_metadata
        try:
            datatype = category_meta[category]['datatype']
        except KeyError:
            datatype = 'text'

        try:
            group = unhexlify(group)
            if isbytestring(group):
                group = group.decode('utf-8')
        except:
            raise cherrypy.HTTPError(404, 'invalid group')

        items = categories[category]
        entries = []
        getter = lambda x: unicode(getattr(x, 'sort', None) or x.name)
        for x in items:
            val = getter(x)
            if not val:
                val = u'A'
            if val.upper().startswith(group):
                entries.append(x)

        sort = self.browse_sort_categories(entries, sort)
        entries = get_category_items(category, entries,
                datatype, self.opts.url_prefix)
        return json.dumps(entries, ensure_ascii=True)

    @Endpoint()
    def browse_catalog(self, category=None, category_sort=None):
        'Entry point for top-level, categories and sub-categories'
        prefix = '' if self.is_wsgi else self.opts.url_prefix
        if category is None:
            ans = self.browse_toplevel()
        # The following are fake categories used for the top-level view
        elif category == 'newest':
            raise cherrypy.InternalRedirect(prefix +
                    '/browse/matches/newest/dummy')
        elif category == 'allbooks':
            raise cherrypy.InternalRedirect(prefix +
                    '/browse/matches/allbooks/dummy')
        elif category == 'randombook':
            raise cherrypy.InternalRedirect(prefix +
                    '/browse/random')
        else:
            ans = self.browse_category(category, category_sort)

        return ans

    # }}}

    # Book Lists {{{

    def browse_sort_book_list(self, items, sort):
        fm = self.db.field_metadata
        keys = frozenset(fm.sortable_field_keys())
        if sort not in keys:
            sort = 'title'
        self.sort(items, 'title', True)
        if sort != 'title':
            ascending = fm[sort]['datatype'] not in ('rating', 'datetime',
                    'series')
            self.sort(items, sort, ascending)
        return sort

    @Endpoint(sort_type='list')
    def browse_matches(self, category=None, cid=None, list_sort=None):
        if list_sort:
            list_sort = unquote(list_sort)
        if not cid:
            raise cherrypy.HTTPError(404, 'invalid category id: %r'%cid)
        categories = self.categories_cache()

        if category not in categories and \
                category not in ('newest', 'allbooks', 'virt_libs'):
            raise cherrypy.HTTPError(404, 'category not found')
        fm = self.db.field_metadata
        try:
            category_name = fm[category]['name']
            dt = fm[category]['datatype']
        except:
            if category not in ('newest', 'allbooks', 'virt_libs'):
                raise
            category_name = {
                    'newest' : _('Newest'),
                    'allbooks' : _('All books'),
                    'virt_libs': _('Virtual Libraries'),
            }[category]
            dt = None

        hide_sort = 'true' if dt == 'series' else 'false'
        if category == 'search':
            which = unhexlify(cid).decode('utf-8')
            try:
                ids = self.search_cache('search:"%s"'%which)
            except:
                raise cherrypy.HTTPError(404, 'Search: %r not understood'%which)
        else:
            all_ids = self.search_cache('')
            if category == 'newest':
                ids = all_ids
                hide_sort = 'true'
            elif category == 'allbooks':
                ids = all_ids
            elif category == 'virt_libs':
                which = unhexlify(cid).decode('utf-8')
                vls = self.db.prefs.get('virtual_libraries', {})
                ids = self.search_cache(vls[which])
                category_name = _('virtual library: ') + xml(which)
                if not ids:
                    msg = _('The virtual library <b>%s</b> has no books.') % prepare_string_for_xml(which)
                    if self.search_restriction:
                        msg += ' ' + _(
                            'This is probably because you have applied a virtual library'
                            ' to the content server in Preferences->Sharing over the net.'
                            ' This virtual library is applied globally and combined with'
                            ' the current virtual library.')
                    return self.browse_template('name').format(title='',
                        main='<p>%s</p>'%msg)
            else:
                if fm.get(category, {'datatype':None})['datatype'] == 'composite':
                    cid = cid.decode('utf-8')
                q = category
                if q == 'news':
                    q = 'tags'
                ids = self.db.get_books_for_category(q, cid)
                ids = [x for x in ids if x in all_ids]

        items = [self.db.data.tablerow_for_id(x) for x in ids]
        if category == 'newest':
            list_sort = 'timestamp'
        if dt == 'series':
            list_sort = category
        sort = self.browse_sort_book_list(items, list_sort)
        ids = [x[0] for x in items]
        html = self.browse_json_books(ids, self.opts.url_prefix,
                suffix=_('in') + ' ' + category_name)

        return self.browse_template(sort, category=False).format(
                title=_('Books in') + " " +category_name,
                main=html)

    def browse_get_book_args(self, mi, id_, add_category_links=False):
        fmts = self.db.formats(id_, index_is_id=True)
        if not fmts:
            fmts = ''
        fmts = [x.lower() for x in fmts.split(',') if x]
        pf = prefs['output_format'].lower()
        try:
            fmt = pf if pf in fmts else fmts[0]
        except:
            fmt = None
        args = {'id':id_, 'mi':mi,
                }
        ccache = self.categories_cache() if add_category_links else {}
        ftitle = fauthors = ''
        for key in mi.all_field_keys():
            val = mi.format_field(key)[1]
            if not val:
                val = ''
            if key == 'title':
                ftitle = xml(val, True)
            elif key == 'authors':
                fauthors = xml(val, True)
            if add_category_links:
                added_key = False
                fm = mi.metadata_for_field(key)
                if val and fm and fm['is_category'] and not fm['is_csp'] and\
                        key != 'formats' and fm['datatype'] not in ['rating']:
                    categories = mi.get(key)
                    if isinstance(categories, basestring):
                        categories = [categories]
                    dbtags = []
                    for category in categories:
                        dbtag = None
                        for tag in ccache[key]:
                            if tag.name == category:
                                dbtag = tag
                                break
                        dbtags.append(dbtag)
                    if None not in dbtags:
                        vals = []
                        for tag in dbtags:
                            tval = ('<a title="Browse books by {3}: {0}"'
                            ' href="{1}" class="details_category_link">{2}</a>')
                            href='%s/browse/matches/%s/%s' % \
                            (self.opts.url_prefix, quote(tag.category), quote(str(tag.id)))
                            vals.append(tval.format(xml(tag.name, True),
                                xml(href, True),
                                xml(val if len(dbtags) == 1 else tag.name),
                                xml(key, True)))
                        join = ' &amp; ' if key == 'authors' or \
                                            (fm['is_custom'] and
                                             fm['display'].get('is_names', False)) \
                                         else ', '
                        args[key] = join.join(vals)
                        added_key = True
                if not added_key:
                    args[key] = xml(val, True)
            else:
                args[key] = xml(val, True)
        fname = quote(ascii_filename(ftitle) + ' - ' +
                ascii_filename(fauthors))
        return args, fmt, fmts, fname

    @Endpoint(mimetype='application/json; charset=utf-8')
    def browse_booklist_page(self, ids=None, sort=None):
        if sort == 'null':
            sort = None
        if ids is None:
            ids = json.dumps('[]')
        try:
            ids = json.loads(ids)
        except:
            raise cherrypy.HTTPError(404, 'invalid ids')
        summs = []
        for id_ in ids:
            try:
                id_ = int(id_)
                mi = self.db.get_metadata(id_, index_is_id=True)
            except:
                continue
            args, fmt, fmts, fname = self.browse_get_book_args(mi, id_)
            args['other_formats'] = ''
            args['fmt'] = fmt
            if fmts and fmt:
                other_fmts = [x for x in fmts if x.lower() != fmt.lower()]
                if other_fmts:
                    ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'
                            .format(f, fname, id_, f.upper(),
                                self.opts.url_prefix) for f in
                            other_fmts]
                    ofmts = ', '.join(ofmts)
                    args['other_formats'] = u'<strong>%s: </strong>' % \
                            _('Other formats') + ofmts

            args['details_href'] = self.opts.url_prefix + '/browse/details/'+str(id_)

            if fmt:
                href = self.opts.url_prefix + '/get/%s/%s_%d.%s'%(
                        fmt, fname, id_, fmt)
                rt = xml(_('Read %(title)s in the %(fmt)s format')%
                        {'title':args['title'], 'fmt':fmt.upper()}, True)

                args['get_button'] = \
                        '<a href="%s" class="read" title="%s">%s</a>' % \
                        (xml(href, True), rt, xml(_('Get')))
                args['get_url'] = xml(href, True)
            else:
                args['get_button'] = ''
                args['get_url'] = 'javascript:alert(\'%s\')' % xml(_(
                    'This book has no available formats to view'), True)
            args['comments'] = comments_to_html(mi.comments)
            args['stars'] = ''
            if mi.rating:
                args['stars'] = render_rating(mi.rating/2.0,
                        self.opts.url_prefix, prefix=_('Rating'))[0]
            if args['tags']:
                args['tags'] = u'<strong>%s: </strong>'%xml(_('Tags')) + \
                    args['tags']
            if args['series']:
                args['series'] = args['series']
            args['details'] = xml(_('Details'), True)
            args['details_tt'] = xml(_('Show book details'), True)
            args['permalink'] = xml(_('Permalink'), True)
            args['permalink_tt'] = xml(_('A permanent link to this book'), True)

            summs.append(self.browse_summary_template.format(**args))

        raw = json.dumps('\n'.join(summs), ensure_ascii=True)
        return raw

    def browse_render_details(self, id_, add_random_button=False, add_title=False):
        try:
            mi = self.db.get_metadata(id_, index_is_id=True)
        except:
            return _('This book has been deleted')
        else:
            args, fmt, fmts, fname = self.browse_get_book_args(mi, id_,
                    add_category_links=True)
            args['fmt'] = fmt
            if fmt:
                args['get_url'] = xml(self.opts.url_prefix + '/get/%s/%s_%d.%s'%(
                    fmt, fname, id_, fmt), True)
            else:
                args['get_url'] = 'javascript:alert(\'%s\')' % xml(_(
                    'This book has no available formats to view'), True)
            args['formats'] = ''
            if fmts:
                ofmts = [u'<a href="{4}/get/{0}/{1}_{2}.{0}" title="{3}">{3}</a>'
                        .format(xfmt, fname, id_, xfmt.upper(),
                            self.opts.url_prefix) for xfmt in fmts]
                ofmts = ', '.join(ofmts)
                args['formats'] = ofmts
            fields, comments = [], []
            displayed_custom_fields = custom_fields_to_display(self.db)
            for field, m in list(mi.get_all_standard_metadata(False).items()) + \
                    list(mi.get_all_user_metadata(False).items()):
                if self.db.field_metadata.is_ignorable_field(field) and \
                                field not in displayed_custom_fields:
                    continue
                if m['datatype'] == 'comments' or field == 'comments' or (
                        m['datatype'] == 'composite' and
                        m['display'].get('contains_html', False)):
                    val = mi.get(field, '')
                    if val and val.strip():
                        comments.append((m['name'], comments_to_html(val)))
                    continue
                if field in ('title', 'formats') or not args.get(field, False) \
                        or not m['name']:
                    continue
                if field == 'identifiers':
                    urls = urls_from_identifiers(mi.get(field, {}))
                    links = [u'<a class="details_category_link" target="_new" href="%s" title="%s:%s">%s</a>' % (url, id_typ, id_val, name)
                            for name, id_typ, id_val, url in urls]
                    links = u', '.join(links)
                    if links:
                        fields.append((field, m['name'], u'<strong>%s: </strong>%s'%(
                            _('Ids'), links)))
                        continue

                if m['datatype'] == 'rating':
                    r = u'<strong>%s: </strong>'%xml(m['name']) + \
                            render_rating(mi.get(field)/2.0, self.opts.url_prefix,
                                    prefix=m['name'])[0]
                else:
                    r = u'<strong>%s: </strong>'%xml(m['name']) + \
                                args[field]
                fields.append((field, m['name'], r))

            def fsort(x):
                num = {'authors':0, 'series':1, 'tags':2}.get(x[0], 100)
                return (num, sort_key(x[-1]))
            fields.sort(key=fsort)
            if add_title:
                fields.insert(0, ('title', 'Title', u'<strong>%s: </strong>%s' % (xml(_('Title')), xml(mi.title))))
            fields = [u'<div class="field">{0}</div>'.format(f[-1]) for f in
                    fields]
            fields = u'<div class="fields">%s</div>'%('\n\n'.join(fields))

            comments.sort(key=lambda x: x[0].lower())
            comments = [(u'<div class="field"><strong>%s: </strong>'
                         u'<div class="comment">%s</div></div>') % (xml(c[0]),
                             c[1]) for c in comments]
            comments = u'<div class="comments">%s</div>'%('\n\n'.join(comments))
            random = ''
            if add_random_button:
                href = '%s/browse/random?v=%s'%(
                    self.opts.url_prefix, time.time())
                random = '<a href="%s" id="random_button" title="%s">%s</a>' % (
                    xml(href, True), xml(_('Choose another random book'), True),
                    xml(_('Another random book')))

            return self.browse_details_template.format(
                id=id_, title=xml(mi.title, True), fields=fields,
                get_url=args['get_url'], fmt=args['fmt'],
                formats=args['formats'], comments=comments, random=random)

    @Endpoint(mimetype='application/json; charset=utf-8')
    def browse_details(self, id=None):
        try:
            id_ = int(id)
        except:
            raise cherrypy.HTTPError(404, 'invalid id: %r'%id)

        ans = self.browse_render_details(id_)

        return json.dumps(ans, ensure_ascii=True)

    @Endpoint()
    def browse_random(self, *args, **kwargs):
        import random
        all_ids = list(self.search_cache(''))
        random.shuffle(all_ids)
        html = self.browse_json_books(all_ids, self.opts.url_prefix)
        return self.browse_template(None).format(
                title=_('Books in'),
                main=html)

    @Endpoint()
    def browse_book(self, id=None, category_sort=None):
        try:
            id_ = int(id)
        except:
            raise cherrypy.HTTPError(404, 'invalid id: %r'%id)
        ans = self.browse_render_details(id_, add_title=True)
        return self.browse_template('').format(
                title=prepare_string_for_xml(self.db.title(id_, index_is_id=True)), script='book();', main=ans)

    # }}}

    # Search {{{
    @Endpoint()
    def browse_search(self, query='', list_sort=None):
        if isbytestring(query):
            query = query.decode('UTF-8')
        ids = self.search_for_books(query)
        items = [self.db.data.tablerow_for_id(x) for x in ids]
        sort = self.browse_sort_book_list(items, list_sort)
        ids = [x[0] for x in items]
        html = render_book_list(ids, self.opts.url_prefix,
                suffix=_('in search')+': '+xml(query))
        return self.browse_template(sort, category=False, initial_search=query).format(
                title=_('Matching books'),
                main=html)
Exemplo n.º 12
0
    def sync_booklists(self, booklists, end_session=True):
        '''
        Update metadata on device.

        :param booklists: A tuple containing the result of calls to
                          (:meth:`books(oncard=None)`,
                          :meth:`books(oncard='carda')`,
                          :meth`books(oncard='cardb')`).

        '''
        if not self.bambook:
            return

        json_codec = JsonCodec()

        # Create stub virtual book for sync info
        with TemporaryDirectory() as tdir:
            snbcdir = os.path.join(tdir, 'snbc')
            snbfdir = os.path.join(tdir, 'snbf')
            os.mkdir(snbcdir)
            os.mkdir(snbfdir)

            f = open(os.path.join(snbfdir, 'book.snbf'), 'wb')
            f.write('''<book-snbf version="1.0">
  <head>
    <name>calibre同步信息</name>
    <author>calibre</author>
    <language>ZH-CN</language>
    <rights/>
    <publisher>calibre</publisher>
    <generator>''' + __appname__ + ' ' + __version__ + '''</generator>
    <created/>
    <abstract></abstract>
    <cover/>
  </head>
</book-snbf>
''')
            f.close()
            f = open(os.path.join(snbfdir, 'toc.snbf'), 'wb')
            f.write('''<toc-snbf>
  <head>
    <chapters>0</chapters>
  </head>
  <body>
  </body>
</toc-snbf>
''')
            f.close()
            cache_name = os.path.join(snbcdir, self.METADATA_CACHE)
            with open(cache_name, 'wb') as f:
                json_codec.encode_to_file(f, booklists[0])

            with TemporaryFile('.snb') as f:
                if self.bambook.PackageSNB(f, tdir):
                    if not self.bambook.SendFile(f, self.METADATA_FILE_GUID):
                        print "Upload failed"
                else:
                    print "Package failed"

        # Clear the _new_book indication, as we are supposed to be done with
        # adding books at this point
        for blist in booklists:
            if blist is not None:
                for book in blist:
                    book._new_book = False

        self.report_progress(1.0, _('Sending metadata to device...'))
Exemplo n.º 13
0
    def sync_booklists(self, booklists, end_session=True):
        '''
        Update metadata on device.

        :param booklists: A tuple containing the result of calls to
                          (:meth:`books(oncard=None)`,
                          :meth:`books(oncard='carda')`,
                          :meth`books(oncard='cardb')`).

        '''
        if not self.bambook:
            return

        json_codec = JsonCodec()

        # Create stub virtual book for sync info
        with TemporaryDirectory() as tdir:
            snbcdir = os.path.join(tdir, 'snbc')
            snbfdir = os.path.join(tdir, 'snbf')
            os.mkdir(snbcdir)
            os.mkdir(snbfdir)

            f = open(os.path.join(snbfdir, 'book.snbf'), 'wb')
            f.write('''<book-snbf version="1.0">
  <head>
    <name>calibre同步信息</name>
    <author>calibre</author>
    <language>ZH-CN</language>
    <rights/>
    <publisher>calibre</publisher>
    <generator>''' + __appname__ + ' ' + __version__ + '''</generator>
    <created/>
    <abstract></abstract>
    <cover/>
  </head>
</book-snbf>
''')
            f.close()
            f = open(os.path.join(snbfdir, 'toc.snbf'), 'wb')
            f.write('''<toc-snbf>
  <head>
    <chapters>0</chapters>
  </head>
  <body>
  </body>
</toc-snbf>
''');
            f.close()
            cache_name = os.path.join(snbcdir, self.METADATA_CACHE)
            with open(cache_name, 'wb') as f:
                json_codec.encode_to_file(f, booklists[0])

            with TemporaryFile('.snb') as f:
                if self.bambook.PackageSNB(f, tdir):
                    if not self.bambook.SendFile(f, self.METADATA_FILE_GUID):
                        print "Upload failed"
                else:
                    print "Package failed"

        # Clear the _new_book indication, as we are supposed to be done with
        # adding books at this point
        for blist in booklists:
            if blist is not None:
                for book in blist:
                    book._new_book = False

        self.report_progress(1.0, _('Sending metadata to device...'))