initial commit
parent
96a4230fda
commit
6849187c2a
|
@ -0,0 +1,310 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
BaseFormatter.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Base class for output formatters.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from six.moves import urllib
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
import genshi.output
|
||||||
|
import genshi.template
|
||||||
|
from genshi.core import _ensure
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
# use a bit more aggressive whitespace removal than the standard whitespace filter
|
||||||
|
COLLAPSE_LINES = re.compile('\n[ \t\r\n]+').sub
|
||||||
|
|
||||||
|
WHITESPACE_FILTER = genshi.output.WhitespaceFilter ()
|
||||||
|
|
||||||
|
DATA_URL_CACHE = {}
|
||||||
|
|
||||||
|
class BaseFormatter (object):
|
||||||
|
""" Base class for formatters. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = 'text/html; charset=UTF-8'
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
self.templates = {}
|
||||||
|
|
||||||
|
|
||||||
|
def format (self, page, os):
|
||||||
|
""" Abstract method to override. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
""" Abstract method to override.
|
||||||
|
|
||||||
|
Like this:
|
||||||
|
return genshi.output.XMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def send_headers (self):
|
||||||
|
""" Send HTTP content-type header. """
|
||||||
|
cherrypy.response.headers['Content-Type'] = self.CONTENT_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
def render (self, page, os):
|
||||||
|
""" Render and send to browser. """
|
||||||
|
|
||||||
|
self.send_headers ()
|
||||||
|
|
||||||
|
template = self.templates[page]
|
||||||
|
ctxt = genshi.template.Context (cherrypy = cherrypy, os = os, bs = BaseSearcher)
|
||||||
|
|
||||||
|
stream = template.stream
|
||||||
|
for filter_ in template.filters:
|
||||||
|
stream = filter_ (iter (stream), ctxt)
|
||||||
|
|
||||||
|
# there's no easy way in genshi to pass collapse_lines to this filter
|
||||||
|
stream = WHITESPACE_FILTER (stream, collapse_lines = COLLAPSE_LINES)
|
||||||
|
|
||||||
|
return genshi.output.encode (self.get_serializer ()(_ensure (genshi.Stream (stream))),
|
||||||
|
encoding = 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def set_template (self, page, template):
|
||||||
|
""" Set template for page.
|
||||||
|
|
||||||
|
Override this for special handling of template, like adding filters. """
|
||||||
|
self.templates[page] = template
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_date (date):
|
||||||
|
""" Format a date. """
|
||||||
|
|
||||||
|
if date is None:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
# datetime
|
||||||
|
return date.replace (tzinfo = gg.UTC (), microsecond = 0).isoformat ()
|
||||||
|
except TypeError:
|
||||||
|
# date
|
||||||
|
return datetime.datetime.combine (
|
||||||
|
date, datetime.time (tzinfo = gg.UTC ())).isoformat ()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def data_url (path):
|
||||||
|
""" Read and convert a file to a data url. """
|
||||||
|
if path in DATA_URL_CACHE:
|
||||||
|
return DATA_URL_CACHE[path]
|
||||||
|
|
||||||
|
abs_path = os.path.join ('http://' + cherrypy.config['file_host'], path.lstrip ('/'))
|
||||||
|
data_url = abs_path
|
||||||
|
try:
|
||||||
|
f = urllib.request.urlopen (abs_path)
|
||||||
|
retcode = f.getcode ()
|
||||||
|
if retcode is None or retcode == 200:
|
||||||
|
msg = f.info ()
|
||||||
|
mediatype = msg.get ('Content-Type')
|
||||||
|
if mediatype:
|
||||||
|
mediatype = mediatype.partition (';')[0]
|
||||||
|
data_url = ('data:' + mediatype + ';base64,' +
|
||||||
|
base64.b64encode (f.read ()).decode ('ascii'))
|
||||||
|
f.close ()
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
DATA_URL_CACHE[path] = data_url
|
||||||
|
return data_url
|
||||||
|
|
||||||
|
|
||||||
|
def fix_dc (self, dc, os):
|
||||||
|
""" Add some info to dc for easier templating. """
|
||||||
|
|
||||||
|
# obsolete private marc codes for cover art
|
||||||
|
dc.marcs = [ marc for marc in dc.marcs if not marc.code.startswith ('9') ]
|
||||||
|
|
||||||
|
dc.cover_image = None
|
||||||
|
dc.cover_thumb = None
|
||||||
|
# cover image really should not be a property of opensearch,
|
||||||
|
# but it is accessed in many places and this way we can save a
|
||||||
|
# lot of iterations later
|
||||||
|
os.cover_image_url = None
|
||||||
|
os.cover_thumb_url = None
|
||||||
|
|
||||||
|
for file_ in dc.files:
|
||||||
|
|
||||||
|
# HACK for https://
|
||||||
|
if file_.url.startswith ('http://'):
|
||||||
|
file_.url = file_.url[5:] # to //
|
||||||
|
|
||||||
|
file_.dropbox_url = None
|
||||||
|
# file_.dropbox_filename = None
|
||||||
|
file_.gdrive_url = None
|
||||||
|
file_.msdrive_url = None
|
||||||
|
file_.honeypot_url = None
|
||||||
|
|
||||||
|
if file_.filetype == 'cover.medium':
|
||||||
|
dc.cover_image = file_
|
||||||
|
os.snippet_image_url = os.cover_image_url = file_.url
|
||||||
|
elif file_.filetype == 'cover.small':
|
||||||
|
dc.cover_thumb = file_
|
||||||
|
os.cover_thumb_url = file_.url
|
||||||
|
|
||||||
|
dc.xsd_release_date_time = self.format_date (dc.release_date)
|
||||||
|
|
||||||
|
if 'Sound' in dc.categories:
|
||||||
|
dc.icon = 'audiobook'
|
||||||
|
|
||||||
|
|
||||||
|
# lifted from genshi/output.py and fixed lang issue
|
||||||
|
# lang is not allowed in xhtml 1.1 which we must use
|
||||||
|
# because xhtml+rdfa is based on it
|
||||||
|
|
||||||
|
from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
|
||||||
|
from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
|
||||||
|
START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
|
||||||
|
from genshi.output import EMPTY, EmptyTagFilter, WhitespaceFilter, \
|
||||||
|
NamespaceFlattener, DocTypeInserter
|
||||||
|
|
||||||
|
class XHTMLSerializer (genshi.output.XMLSerializer):
|
||||||
|
"""Produces XHTML text from an event stream.
|
||||||
|
|
||||||
|
>>> from genshi.builder import tag
|
||||||
|
>>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
|
||||||
|
>>> print(''.join(XHTMLSerializer()(elem.generate())))
|
||||||
|
<div><a href="foo"></a><br /><hr noshade="noshade" /></div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
_EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
|
||||||
|
'hr', 'img', 'input', 'isindex', 'link', 'meta',
|
||||||
|
'param'])
|
||||||
|
_BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
|
||||||
|
'defer', 'disabled', 'ismap', 'multiple',
|
||||||
|
'nohref', 'noresize', 'noshade', 'nowrap'])
|
||||||
|
_PRESERVE_SPACE = frozenset([
|
||||||
|
QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
|
||||||
|
QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
|
||||||
|
])
|
||||||
|
|
||||||
|
def __init__(self, doctype=None, strip_whitespace=True,
|
||||||
|
namespace_prefixes=None, drop_xml_decl=True, cache=True):
|
||||||
|
super(XHTMLSerializer, self).__init__(doctype, False)
|
||||||
|
self.filters = [EmptyTagFilter()]
|
||||||
|
if strip_whitespace:
|
||||||
|
self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
|
||||||
|
namespace_prefixes = namespace_prefixes or {}
|
||||||
|
namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
|
||||||
|
self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
|
||||||
|
cache=cache))
|
||||||
|
if doctype:
|
||||||
|
self.filters.append(DocTypeInserter(doctype))
|
||||||
|
self.drop_xml_decl = drop_xml_decl
|
||||||
|
self.cache = cache
|
||||||
|
|
||||||
|
def __call__(self, stream):
|
||||||
|
boolean_attrs = self._BOOLEAN_ATTRS
|
||||||
|
empty_elems = self._EMPTY_ELEMS
|
||||||
|
drop_xml_decl = self.drop_xml_decl
|
||||||
|
have_decl = have_doctype = False
|
||||||
|
in_cdata = False
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
cache_get = cache.get
|
||||||
|
if self.cache:
|
||||||
|
def _emit(kind, input, output):
|
||||||
|
cache[kind, input] = output
|
||||||
|
return output
|
||||||
|
else:
|
||||||
|
def _emit(kind, input, output):
|
||||||
|
return output
|
||||||
|
|
||||||
|
for filter_ in self.filters:
|
||||||
|
stream = filter_(stream)
|
||||||
|
for kind, data, pos in stream:
|
||||||
|
cached = cache_get((kind, data))
|
||||||
|
if cached is not None:
|
||||||
|
yield cached
|
||||||
|
|
||||||
|
elif kind is START or kind is EMPTY:
|
||||||
|
tag, attrib = data
|
||||||
|
buf = ['<', tag]
|
||||||
|
for attr, value in attrib:
|
||||||
|
if attr in boolean_attrs:
|
||||||
|
value = attr
|
||||||
|
# this is the fix
|
||||||
|
# elif attr == 'xml:lang' and 'lang' not in attrib:
|
||||||
|
# buf += [' lang="', escape(value), '"']
|
||||||
|
elif attr == 'xml:space':
|
||||||
|
continue
|
||||||
|
buf += [' ', attr, '="', escape(value), '"']
|
||||||
|
if kind is EMPTY:
|
||||||
|
if tag in empty_elems:
|
||||||
|
buf.append(' />')
|
||||||
|
else:
|
||||||
|
buf.append('></%s>' % tag)
|
||||||
|
else:
|
||||||
|
buf.append('>')
|
||||||
|
yield _emit(kind, data, Markup(''.join(buf)))
|
||||||
|
|
||||||
|
elif kind is END:
|
||||||
|
yield _emit(kind, data, Markup('</%s>' % data))
|
||||||
|
|
||||||
|
elif kind is TEXT:
|
||||||
|
if in_cdata:
|
||||||
|
yield _emit(kind, data, data)
|
||||||
|
else:
|
||||||
|
yield _emit(kind, data, escape(data, quotes=False))
|
||||||
|
|
||||||
|
elif kind is COMMENT:
|
||||||
|
yield _emit(kind, data, Markup('<!--%s-->' % data))
|
||||||
|
|
||||||
|
elif kind is DOCTYPE and not have_doctype:
|
||||||
|
name, pubid, sysid = data
|
||||||
|
buf = ['<!DOCTYPE %s']
|
||||||
|
if pubid:
|
||||||
|
buf.append(' PUBLIC "%s"')
|
||||||
|
elif sysid:
|
||||||
|
buf.append(' SYSTEM')
|
||||||
|
if sysid:
|
||||||
|
buf.append(' "%s"')
|
||||||
|
buf.append('>\n')
|
||||||
|
yield Markup(''.join(buf)) % tuple([p for p in data if p])
|
||||||
|
have_doctype = True
|
||||||
|
|
||||||
|
elif kind is XML_DECL and not have_decl and not drop_xml_decl:
|
||||||
|
version, encoding, standalone = data
|
||||||
|
buf = ['<?xml version="%s"' % version]
|
||||||
|
if encoding:
|
||||||
|
buf.append(' encoding="%s"' % encoding)
|
||||||
|
if standalone != -1:
|
||||||
|
standalone = standalone and 'yes' or 'no'
|
||||||
|
buf.append(' standalone="%s"' % standalone)
|
||||||
|
buf.append('?>\n')
|
||||||
|
yield Markup(''.join(buf))
|
||||||
|
have_decl = True
|
||||||
|
|
||||||
|
elif kind is START_CDATA:
|
||||||
|
yield Markup('<![CDATA[')
|
||||||
|
in_cdata = True
|
||||||
|
|
||||||
|
elif kind is END_CDATA:
|
||||||
|
yield Markup(']]>')
|
||||||
|
in_cdata = False
|
||||||
|
|
||||||
|
elif kind is PI:
|
||||||
|
yield _emit(kind, data, Markup('<?%s %s?>' % data))
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,131 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
BibrecPage.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The bibrec page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import Page
|
||||||
|
|
||||||
|
|
||||||
|
class BibrecPage (Page.Page):
|
||||||
|
""" Implements the bibrec page. """
|
||||||
|
|
||||||
|
def index (self, **dummy_kwargs):
|
||||||
|
""" A bibrec page. """
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
|
||||||
|
os.log_request ('bibrec')
|
||||||
|
|
||||||
|
dc = BaseSearcher.DC (cherrypy.engine.pool)
|
||||||
|
|
||||||
|
# the bulk of the work is done here
|
||||||
|
dc.load_from_database (os.id)
|
||||||
|
if not dc.files:
|
||||||
|
# NOTE: Error message
|
||||||
|
cherrypy.tools.rate_limiter.e404 ()
|
||||||
|
raise cherrypy.HTTPError (404, _('No ebook by that number.'))
|
||||||
|
|
||||||
|
# add these fields so we won't have to test for their existence later
|
||||||
|
dc.extra_info = None
|
||||||
|
dc.url = None
|
||||||
|
|
||||||
|
dc.translate ()
|
||||||
|
dc.header = gg.cut_at_newline (dc.title)
|
||||||
|
os.title = dc.make_pretty_title ()
|
||||||
|
dc.extra_info = ''
|
||||||
|
dc.class_ = BaseSearcher.ClassAttr ()
|
||||||
|
dc.order = 10
|
||||||
|
dc.icon = 'book'
|
||||||
|
if 'Sound' in dc.categories:
|
||||||
|
dc.icon = 'audiobook'
|
||||||
|
os.title_icon = dc.icon
|
||||||
|
os.twit = os.title
|
||||||
|
os.qrcode_url = '//%s/cache/epub/%d/pg%d.qrcode.png' % (os.file_host, os.id, os.id)
|
||||||
|
|
||||||
|
os.entries.append (dc)
|
||||||
|
|
||||||
|
s = cherrypy.session
|
||||||
|
last_visited = s.get ('last_visited', [])
|
||||||
|
last_visited.append (os.id)
|
||||||
|
s['last_visited'] = last_visited
|
||||||
|
|
||||||
|
# can we find some meaningful breadcrumbs ?
|
||||||
|
for a in dc.authors:
|
||||||
|
if a.marcrel in ('aut', 'cre'):
|
||||||
|
book_cnt = BaseSearcher.sql_get (
|
||||||
|
"select count (*) from mn_books_authors where fk_authors = %(aid)s", aid = a.id)
|
||||||
|
if book_cnt > 1:
|
||||||
|
os.breadcrumbs.append ((
|
||||||
|
__('One by {author}', '{count} by {author}', book_cnt).format (
|
||||||
|
count = book_cnt, author = dc.make_pretty_name (a.name)),
|
||||||
|
_('Find more ebooks by the same author.'),
|
||||||
|
os.url ('author', id = a.id)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
if os.format in ('html', 'mobile'):
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.header = _('Similar Books')
|
||||||
|
cat.title = _('Readers also downloaded…')
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.url = os.url ('also', id = os.id)
|
||||||
|
cat.class_ += 'navlink grayed noprint'
|
||||||
|
cat.icon = 'suggestion'
|
||||||
|
cat.order = 30
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
for bookshelf in dc.bookshelves:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('In {bookshelf}').format (bookshelf = bookshelf.bookshelf)
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.url = os.url ('bookshelf', id = bookshelf.id)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'bookshelf'
|
||||||
|
cat.order = 33
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
if os.format in ('mobile', ):
|
||||||
|
for author in dc.authors:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('By {author}').format (author = author.name_and_dates)
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.url = os.url ('author', id = author.id)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'author'
|
||||||
|
cat.order = 31
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
for subject in dc.subjects:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('On {subject}').format (subject = subject.subject)
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.url = os.url ('subject', id = subject.id)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'subject'
|
||||||
|
cat.order = 32
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
os.total_results = 1
|
||||||
|
|
||||||
|
os.template = 'results' if os.format == 'mobile' else 'bibrec'
|
||||||
|
os.page = 'bibrec'
|
||||||
|
os.og_type = 'book'
|
||||||
|
os.finalize ()
|
||||||
|
|
||||||
|
return self.format (os)
|
|
@ -0,0 +1,146 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
CaptchaPage.py
|
||||||
|
|
||||||
|
Copyright 2013-14 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Serve a captcha page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import cherrypy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import Page
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
#
|
||||||
|
# reCaptcha API docs:
|
||||||
|
# https://developers.google.com/recaptcha/docs/verify
|
||||||
|
#
|
||||||
|
|
||||||
|
API = "http://www.google.com/recaptcha/api/verify"
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionPage (Page.Page):
|
||||||
|
""" Output captcha page. """
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output captcha. """
|
||||||
|
|
||||||
|
cherrypy.lib.caching.expires (3600, True)
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
os.template = 'recaptcha'
|
||||||
|
os.recaptcha_public_key = cherrypy.config['recaptcha_public_key']
|
||||||
|
os.error = kwargs.get ('error')
|
||||||
|
os.finalize ()
|
||||||
|
|
||||||
|
# Remove Session cookie, so that page can be cached.
|
||||||
|
name = cherrypy.serving.request.config.get ('tools.sessions.name', 'session_id')
|
||||||
|
del cherrypy.serving.response.cookie[name]
|
||||||
|
|
||||||
|
return self.format (os)
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerPage (object):
|
||||||
|
""" Check answer with google. """
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Check with google. """
|
||||||
|
|
||||||
|
cherrypy.lib.caching.expires (0, True)
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
|
||||||
|
# Remove Session cookie.
|
||||||
|
name = cherrypy.serving.request.config.get ('tools.sessions.name', 'session_id')
|
||||||
|
del cherrypy.serving.response.cookie[name]
|
||||||
|
|
||||||
|
if 'recaptcha_challenge_field' in kwargs:
|
||||||
|
response = submit (
|
||||||
|
kwargs['recaptcha_challenge_field'],
|
||||||
|
kwargs['recaptcha_response_field'],
|
||||||
|
cherrypy.config['recaptcha_private_key'],
|
||||||
|
cherrypy.request.remote.ip)
|
||||||
|
|
||||||
|
cherrypy.ipsession.captcha_answer (response)
|
||||||
|
|
||||||
|
if not response.is_valid:
|
||||||
|
raise cherrypy.HTTPRedirect (
|
||||||
|
os.url ('captcha.question', error = 'incorrect-captcha-sol'))
|
||||||
|
|
||||||
|
for req in reversed (cherrypy.ipsession['requests']):
|
||||||
|
if 'captcha' not in req:
|
||||||
|
raise cherrypy.HTTPRedirect (req)
|
||||||
|
|
||||||
|
raise cherrypy.HTTPRedirect (os.url ('start'))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Following is stolen from pypi package recaptcha-client 1.0.6
|
||||||
|
# http://code.google.com/p/recaptcha/
|
||||||
|
# to make it compatible with Python 3 requests.
|
||||||
|
#
|
||||||
|
|
||||||
|
class RecaptchaResponse (object):
|
||||||
|
""" The response from the reCaptcha server. """
|
||||||
|
|
||||||
|
def __init__ (self, is_valid, error_code = None):
|
||||||
|
self.is_valid = is_valid
|
||||||
|
self.error_code = error_code
|
||||||
|
|
||||||
|
|
||||||
|
def submit (recaptcha_challenge_field,
|
||||||
|
recaptcha_response_field,
|
||||||
|
private_key,
|
||||||
|
remoteip):
|
||||||
|
"""
|
||||||
|
Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
|
||||||
|
for the request
|
||||||
|
|
||||||
|
recaptcha_challenge_field -- The value of recaptcha_challenge_field from the form
|
||||||
|
recaptcha_response_field -- The value of recaptcha_response_field from the form
|
||||||
|
private_key -- your reCAPTCHA private key
|
||||||
|
remoteip -- the user's ip address
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (recaptcha_response_field and recaptcha_challenge_field and
|
||||||
|
len (recaptcha_response_field) and len (recaptcha_challenge_field)):
|
||||||
|
return RecaptchaResponse (is_valid = False, error_code = 'incorrect-captcha-sol')
|
||||||
|
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'privatekey': private_key,
|
||||||
|
'remoteip': remoteip,
|
||||||
|
'challenge': recaptcha_challenge_field,
|
||||||
|
'response': recaptcha_response_field,
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"User-agent": "reCAPTCHA Python"
|
||||||
|
}
|
||||||
|
|
||||||
|
cherrypy.log ('Data=' + repr (data), context = 'CAPTCHA', severity = logging.INFO)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post (API, data = data, headers = headers)
|
||||||
|
r.raise_for_status ()
|
||||||
|
|
||||||
|
lines = r.text.splitlines ()
|
||||||
|
|
||||||
|
cherrypy.log ('Response=' + "/".join (lines), context = 'CAPTCHA', severity = logging.INFO)
|
||||||
|
|
||||||
|
if lines[0] == "true":
|
||||||
|
return RecaptchaResponse (is_valid = True)
|
||||||
|
else:
|
||||||
|
return RecaptchaResponse (is_valid = False, error_code = lines[1])
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as what:
|
||||||
|
cherrypy.log (str (what), context = 'CAPTCHA', severity = logging.ERROR)
|
||||||
|
return RecaptchaResponse (is_valid = False, error_code = 'recaptcha-not-reachable')
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
CherryPyApp.py
|
||||||
|
|
||||||
|
Copyright 2009-2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The Project Gutenberg Catalog App Server.
|
||||||
|
|
||||||
|
Config and route setup.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.handlers # rotating file handler
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
from cherrypy.process import plugins
|
||||||
|
import six
|
||||||
|
from six.moves import builtins
|
||||||
|
|
||||||
|
from libgutenberg import GutenbergDatabase
|
||||||
|
|
||||||
|
import i18n_tool
|
||||||
|
# Make translator functions available everywhere. Do this early, at
|
||||||
|
# least before Genshi starts loading templates.
|
||||||
|
builtins._ = i18n_tool.ugettext
|
||||||
|
builtins.__ = i18n_tool.ungettext
|
||||||
|
|
||||||
|
import ConnectionPool
|
||||||
|
import Page
|
||||||
|
import StartPage
|
||||||
|
import SuggestionsPage
|
||||||
|
from SearchPage import BookSearchPage, AuthorSearchPage, SubjectSearchPage, BookshelfSearchPage, \
|
||||||
|
AuthorPage, SubjectPage, BookshelfPage, AlsoDownloadedPage
|
||||||
|
from BibrecPage import BibrecPage
|
||||||
|
import CoverPages
|
||||||
|
import QRCodePage
|
||||||
|
import StatsPage
|
||||||
|
import CaptchaPage
|
||||||
|
import Sitemap
|
||||||
|
import Formatters
|
||||||
|
import RateLimiter
|
||||||
|
|
||||||
|
import Timer
|
||||||
|
import MyRamSession
|
||||||
|
import PostgresSession
|
||||||
|
|
||||||
|
cherrypy.lib.sessions.RamSession = MyRamSession.FixedRamSession
|
||||||
|
cherrypy.lib.sessions.MyramSession = MyRamSession.MyRamSession
|
||||||
|
cherrypy.lib.sessions.PostgresSession = PostgresSession.PostgresSession
|
||||||
|
|
||||||
|
plugins.Timer = Timer.TimerPlugin
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
CHERRYPY_CONFIG = ('/etc/autocat3.conf', os.path.expanduser ('~/.autocat3'))
|
||||||
|
# CCHERRYPY_CONFIG = ('/etc/autocat3.conf')
|
||||||
|
else:
|
||||||
|
CHERRYPY_CONFIG = ('/etc/autocat.conf', os.path.expanduser ('~/.autocat'))
|
||||||
|
|
||||||
|
class MyRoutesDispatcher (cherrypy.dispatch.RoutesDispatcher):
|
||||||
|
""" Dispatcher that tells us the matched route.
|
||||||
|
|
||||||
|
CherryPy makes it hard for us by forgetting the matched route object.
|
||||||
|
Here we add a 'route_name' parameter, that will tell us the route's name.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def connect (self, name, route, controller, **kwargs):
|
||||||
|
""" Add a 'route_name' parameter that will tell us the matched route. """
|
||||||
|
kwargs['route_name'] = name
|
||||||
|
kwargs.setdefault ('action', 'index')
|
||||||
|
cherrypy.dispatch.RoutesDispatcher.connect (self, name, route, controller, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def main ():
|
||||||
|
""" Main function. """
|
||||||
|
|
||||||
|
# default config
|
||||||
|
cherrypy.config.update ({
|
||||||
|
'uid': 0,
|
||||||
|
'gid': 0,
|
||||||
|
'server_name': 'localhost',
|
||||||
|
'genshi.template_dir': os.path.join (
|
||||||
|
os.path.dirname (os.path.abspath (__file__)), 'templates'),
|
||||||
|
'daemonize': False,
|
||||||
|
'pidfile': None,
|
||||||
|
'host': 'localhost',
|
||||||
|
'host_mobile': 'localhost',
|
||||||
|
'file_host': 'localhost',
|
||||||
|
})
|
||||||
|
|
||||||
|
config_filename = None
|
||||||
|
for config_filename in CHERRYPY_CONFIG:
|
||||||
|
try:
|
||||||
|
cherrypy.config.update (config_filename)
|
||||||
|
break
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Rotating Logs
|
||||||
|
#
|
||||||
|
|
||||||
|
# Remove the default FileHandlers if present.
|
||||||
|
|
||||||
|
error_file = cherrypy.log.error_file
|
||||||
|
access_file = cherrypy.log.access_file
|
||||||
|
cherrypy.log.error_file = ""
|
||||||
|
cherrypy.log.access_file = ""
|
||||||
|
|
||||||
|
max_bytes = getattr (cherrypy.log, "rot_max_bytes", 100 * 1024 * 1024)
|
||||||
|
backup_count = getattr (cherrypy.log, "rot_backup_count", 2)
|
||||||
|
#print(os.path.abspath(error_file)+": Filehandler cherrypy")
|
||||||
|
h = logging.handlers.RotatingFileHandler (error_file, 'a', max_bytes, backup_count, 'utf-8')
|
||||||
|
h.setLevel (logging.DEBUG)
|
||||||
|
h.setFormatter (cherrypy._cplogging.logfmt)
|
||||||
|
cherrypy.log.error_log.addHandler (h)
|
||||||
|
|
||||||
|
h = logging.handlers.RotatingFileHandler (access_file, 'a', max_bytes, backup_count, 'utf-8')
|
||||||
|
h.setLevel (logging.DEBUG)
|
||||||
|
h.setFormatter (cherrypy._cplogging.logfmt)
|
||||||
|
cherrypy.log.access_log.addHandler (h)
|
||||||
|
|
||||||
|
if not cherrypy.config['daemonize']:
|
||||||
|
ch = logging.StreamHandler ()
|
||||||
|
ch.setLevel (logging.DEBUG)
|
||||||
|
ch.setFormatter (cherrypy._cplogging.logfmt)
|
||||||
|
cherrypy.log.error_log.addHandler (ch)
|
||||||
|
|
||||||
|
# continue app init
|
||||||
|
#
|
||||||
|
|
||||||
|
cherrypy.log ('*' * 80, context = 'ENGINE', severity = logging.INFO)
|
||||||
|
cherrypy.log ("Using config file '%s'." % config_filename,
|
||||||
|
context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
# after cherrypy.config is parsed
|
||||||
|
Formatters.init ()
|
||||||
|
cherrypy.log ("Continuing App Init", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cherrypy.tools.rate_limiter = RateLimiter.RateLimiterTool ()
|
||||||
|
except Exception as e:
|
||||||
|
tb = traceback.format_exc ()
|
||||||
|
cherrypy.log (tb, context = 'ENGINE', severity = logging.ERROR)
|
||||||
|
|
||||||
|
cherrypy.log ("Continuing App Init", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
cherrypy.tools.I18nTool = i18n_tool.I18nTool ()
|
||||||
|
|
||||||
|
cherrypy.log ("Continuing App Init", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
# Used to bust the cache on js and css files. This should be the
|
||||||
|
# files' mtime, but the files are not stored on the app server.
|
||||||
|
# This is a `good enough´ replacement though.
|
||||||
|
t = str (int (time.time ()))
|
||||||
|
cherrypy.config['css_mtime'] = t
|
||||||
|
cherrypy.config['js_mtime'] = t
|
||||||
|
|
||||||
|
cherrypy.config['all_hosts'] = (
|
||||||
|
cherrypy.config['host'], cherrypy.config['host_mobile'], cherrypy.config['file_host'])
|
||||||
|
|
||||||
|
if hasattr (cherrypy.engine, 'signal_handler'):
|
||||||
|
cherrypy.engine.signal_handler.subscribe ()
|
||||||
|
|
||||||
|
cherrypy.engine.pool = plugins.ConnectionPool (
|
||||||
|
cherrypy.engine, params = GutenbergDatabase.get_connection_params (cherrypy.config))
|
||||||
|
cherrypy.engine.pool.subscribe ()
|
||||||
|
|
||||||
|
plugins.RateLimiterReset (cherrypy.engine).subscribe ()
|
||||||
|
plugins.RateLimiterDatabase (cherrypy.engine).subscribe ()
|
||||||
|
plugins.Timer (cherrypy.engine).subscribe ()
|
||||||
|
|
||||||
|
cherrypy.log ("Daemonizing", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
if cherrypy.config['daemonize']:
|
||||||
|
plugins.Daemonizer (cherrypy.engine).subscribe ()
|
||||||
|
|
||||||
|
uid = cherrypy.config['uid']
|
||||||
|
gid = cherrypy.config['gid']
|
||||||
|
if uid > 0 or gid > 0:
|
||||||
|
plugins.DropPrivileges (cherrypy.engine, uid = uid, gid = gid, umask = 0o22).subscribe ()
|
||||||
|
|
||||||
|
if cherrypy.config['pidfile']:
|
||||||
|
pid = plugins.PIDFile (cherrypy.engine, cherrypy.config['pidfile'])
|
||||||
|
# Write pidfile after privileges are dropped (prio == 77)
|
||||||
|
# or we will not be able to remove it.
|
||||||
|
cherrypy.engine.subscribe ('start', pid.start, 78)
|
||||||
|
cherrypy.engine.subscribe ('exit', pid.exit, 78)
|
||||||
|
|
||||||
|
|
||||||
|
cherrypy.log ("Setting up routes", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
# setup 'routes' dispatcher
|
||||||
|
#
|
||||||
|
# d = cherrypy.dispatch.RoutesDispatcher (full_result = True)
|
||||||
|
d = MyRoutesDispatcher (full_result = True)
|
||||||
|
cherrypy.routes_mapper = d.mapper
|
||||||
|
|
||||||
|
def check_id (environ, result):
|
||||||
|
""" Check if id is a valid number. """
|
||||||
|
try:
|
||||||
|
return str (int (result['id'])) == result['id']
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
d.connect ('start', r'/ebooks{.format}/',
|
||||||
|
controller = StartPage.Start ())
|
||||||
|
|
||||||
|
d.connect ('suggest', r'/ebooks/suggest{.format}/',
|
||||||
|
controller = SuggestionsPage.Suggestions ())
|
||||||
|
|
||||||
|
# search pages
|
||||||
|
|
||||||
|
d.connect ('search', r'/ebooks/search{.format}/',
|
||||||
|
controller = BookSearchPage ())
|
||||||
|
|
||||||
|
d.connect ('author_search', r'/ebooks/authors/search{.format}/',
|
||||||
|
controller = AuthorSearchPage ())
|
||||||
|
|
||||||
|
d.connect ('subject_search', r'/ebooks/subjects/search{.format}/',
|
||||||
|
controller = SubjectSearchPage ())
|
||||||
|
|
||||||
|
d.connect ('bookshelf_search', r'/ebooks/bookshelves/search{.format}/',
|
||||||
|
controller = BookshelfSearchPage ())
|
||||||
|
|
||||||
|
# 'id' pages
|
||||||
|
|
||||||
|
d.connect ('author', r'/ebooks/author/{id:\d+}{.format}',
|
||||||
|
controller = AuthorPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
d.connect ('subject', r'/ebooks/subject/{id:\d+}{.format}',
|
||||||
|
controller = SubjectPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
d.connect ('bookshelf', r'/ebooks/bookshelf/{id:\d+}{.format}',
|
||||||
|
controller = BookshelfPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
d.connect ('also', r'/ebooks/{id:\d+}/also/{.format}',
|
||||||
|
controller = AlsoDownloadedPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
# bibrec pages
|
||||||
|
|
||||||
|
d.connect ('download', r'/ebooks/{id:\d+}/download{.format}',
|
||||||
|
controller = Page.NullPage (), _static = True)
|
||||||
|
|
||||||
|
d.connect ('bibrec', r'/ebooks/{id:\d+}{.format}',
|
||||||
|
controller = BibrecPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
|
||||||
|
# legacy compatibility with /ebooks/123.bibrec
|
||||||
|
d.connect ('bibrec2', r'/ebooks/{id:\d+}.bibrec{.format}',
|
||||||
|
controller = BibrecPage (), conditions = dict (function = check_id))
|
||||||
|
|
||||||
|
d.connect ('cover', r'/covers/{size:small|medium}/{order:latest|popular}/{count}',
|
||||||
|
controller = CoverPages.CoverPages ())
|
||||||
|
|
||||||
|
d.connect ('qrcode', r'/qrcode/',
|
||||||
|
controller = QRCodePage.QRCodePage ())
|
||||||
|
|
||||||
|
d.connect ('iplimit', r'/iplimit/',
|
||||||
|
controller = Page.NullPage ())
|
||||||
|
|
||||||
|
d.connect ('stats', r'/stats/',
|
||||||
|
controller = StatsPage.StatsPage ())
|
||||||
|
|
||||||
|
d.connect ('block', r'/stats/block/',
|
||||||
|
controller = RateLimiter.BlockPage ())
|
||||||
|
|
||||||
|
d.connect ('unblock', r'/stats/unblock/',
|
||||||
|
controller = RateLimiter.UnblockPage ())
|
||||||
|
|
||||||
|
d.connect ('traceback', r'/stats/traceback/',
|
||||||
|
controller = RateLimiter.TracebackPage ())
|
||||||
|
|
||||||
|
d.connect ('honeypot_send', r'/ebooks/send/megaupload/{id:\d+}.{filetype}',
|
||||||
|
controller = Page.NullPage (), _static = True)
|
||||||
|
|
||||||
|
# /w/captcha/question/ so varnish will cache it
|
||||||
|
d.connect ('captcha.question', r'/w/captcha/question/',
|
||||||
|
controller = CaptchaPage.QuestionPage ())
|
||||||
|
|
||||||
|
d.connect ('captcha.answer', r'/w/captcha/answer/',
|
||||||
|
controller = CaptchaPage.AnswerPage ())
|
||||||
|
|
||||||
|
# sitemap protocol access control requires us to place sitemaps in /ebooks/
|
||||||
|
d.connect ('sitemap', r'/ebooks/sitemaps/',
|
||||||
|
controller = Sitemap.SitemapIndex ())
|
||||||
|
|
||||||
|
d.connect ('sitemap_index', r'/ebooks/sitemaps/{page:\d+}',
|
||||||
|
controller = Sitemap.Sitemap ())
|
||||||
|
|
||||||
|
if 'dropbox_client_id' in cherrypy.config:
|
||||||
|
import Dropbox
|
||||||
|
dropbox = Dropbox.Dropbox ()
|
||||||
|
cherrypy.log ("Dropbox Client Id: %s" % cherrypy.config['dropbox_client_id'],
|
||||||
|
context = 'ENGINE', severity = logging.INFO)
|
||||||
|
d.connect ('dropbox_send', r'/ebooks/send/dropbox/{id:\d+}.{filetype}',
|
||||||
|
controller = dropbox, conditions = dict (function = check_id))
|
||||||
|
d.connect ('dropbox_callback', r'/ebooks/send/dropbox/',
|
||||||
|
controller = dropbox)
|
||||||
|
|
||||||
|
if 'gdrive_client_id' in cherrypy.config:
|
||||||
|
import GDrive
|
||||||
|
gdrive = GDrive.GDrive ()
|
||||||
|
cherrypy.log ("GDrive Client Id: %s" % cherrypy.config['gdrive_client_id'],
|
||||||
|
context = 'ENGINE', severity = logging.INFO)
|
||||||
|
d.connect ('gdrive_send', r'/ebooks/send/gdrive/{id:\d+}.{filetype}',
|
||||||
|
controller = gdrive, conditions = dict (function = check_id))
|
||||||
|
d.connect ('gdrive_callback', r'/ebooks/send/gdrive/',
|
||||||
|
controller = gdrive)
|
||||||
|
|
||||||
|
if 'msdrive_client_id' in cherrypy.config:
|
||||||
|
import MSDrive
|
||||||
|
msdrive = MSDrive.MSDrive ()
|
||||||
|
cherrypy.log ("MSDrive Client Id: %s" % cherrypy.config['msdrive_client_id'],
|
||||||
|
context = 'ENGINE', severity = logging.INFO)
|
||||||
|
d.connect ('msdrive_send', r'/ebooks/send/msdrive/{id:\d+}.{filetype}',
|
||||||
|
controller = msdrive, conditions = dict (function = check_id))
|
||||||
|
d.connect ('msdrive_callback', r'/ebooks/send/msdrive/',
|
||||||
|
controller = msdrive)
|
||||||
|
|
||||||
|
# start http server
|
||||||
|
#
|
||||||
|
|
||||||
|
cherrypy.log ("Mounting root", context = 'ENGINE', severity = logging.INFO)
|
||||||
|
|
||||||
|
app = cherrypy.tree.mount (root = None, config = config_filename)
|
||||||
|
|
||||||
|
app.merge ({'/': {'request.dispatch': d}})
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main ()
|
||||||
|
cherrypy.engine.start ()
|
||||||
|
cherrypy.engine.block ()
|
|
@ -0,0 +1,303 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
CloudStorage.py
|
||||||
|
|
||||||
|
Copyright 2013-15 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Base classes for uploads to file hosting services.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
from six.moves import urllib
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import routes
|
||||||
|
import requests
|
||||||
|
import requests_oauthlib
|
||||||
|
|
||||||
|
from requests import RequestException
|
||||||
|
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
# pylint: disable=R0921
|
||||||
|
|
||||||
|
http_adapter = requests.adapters.HTTPAdapter ()
|
||||||
|
https_adapter = requests.adapters.HTTPAdapter ()
|
||||||
|
|
||||||
|
# Google Drive `bug´ see:
|
||||||
|
# https://github.com/idan/oauthlib/commit/ca4811b3087f9d34754d3debf839e247593b8a39
|
||||||
|
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
|
||||||
|
|
||||||
|
def log (msg):
|
||||||
|
""" Log an informational message. """
|
||||||
|
cherrypy.log (msg, context = 'CLOUDSTORAGE', severity = logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def error_log (msg):
|
||||||
|
""" Log an error message. """
|
||||||
|
cherrypy.log ('Error: ' + msg, context = 'CLOUDSTORAGE', severity = logging.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class CloudOAuth2Session (requests_oauthlib.OAuth2Session): # pylint: disable=R0904
|
||||||
|
""" An OAuth2 session. """
|
||||||
|
|
||||||
|
name_prefix = None
|
||||||
|
oauth2_auth_endpoint = None
|
||||||
|
oauth2_token_endpoint = None
|
||||||
|
oauth2_scope = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__ (self, **kwargs):
|
||||||
|
""" Initialize session from cherrypy config. """
|
||||||
|
|
||||||
|
config = cherrypy.config
|
||||||
|
prefix = self.name_prefix
|
||||||
|
|
||||||
|
host = config['file_host']
|
||||||
|
urlgen = routes.URLGenerator (cherrypy.routes_mapper, {
|
||||||
|
'HTTP_HOST': host,
|
||||||
|
'HTTPS': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
client_id = config[prefix + '_client_id']
|
||||||
|
redirect_uri = urlgen (prefix + '_callback', host = host)
|
||||||
|
|
||||||
|
super (CloudOAuth2Session, self).__init__ (
|
||||||
|
client_id = client_id,
|
||||||
|
scope = self.oauth2_scope,
|
||||||
|
redirect_uri = redirect_uri,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self.client_secret = config[prefix + '_client_secret']
|
||||||
|
self.ebook = None
|
||||||
|
|
||||||
|
self.mount ("http://", http_adapter)
|
||||||
|
self.mount ("https://", https_adapter)
|
||||||
|
|
||||||
|
|
||||||
|
def oauth_dance (self, kwargs):
|
||||||
|
""" Do the OAuth2 dance. """
|
||||||
|
|
||||||
|
#
|
||||||
|
# OAuth 2.0 flow see:
|
||||||
|
# http://tools.ietf.org/html/rfc6749
|
||||||
|
#
|
||||||
|
|
||||||
|
if not self.token:
|
||||||
|
if 'code' not in kwargs:
|
||||||
|
# oauth step 1:
|
||||||
|
# redirect the user to the Authorization Endpoint
|
||||||
|
log ('Building auth url ...')
|
||||||
|
auth_url, dummy_state = self.authorization_url (
|
||||||
|
self.oauth2_auth_endpoint)
|
||||||
|
log ('Redirecting user to auth endpoint ...')
|
||||||
|
raise cherrypy.HTTPRedirect (auth_url)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# oauth step 2
|
||||||
|
# the user's browser just came back with an authorization code
|
||||||
|
# get the access_token from the Token Endpoint
|
||||||
|
log ('Fetching access token ...')
|
||||||
|
self.fetch_token (self.oauth2_token_endpoint,
|
||||||
|
client_secret = self.client_secret,
|
||||||
|
code = kwargs['code'])
|
||||||
|
log ('Got access token.')
|
||||||
|
|
||||||
|
|
||||||
|
def unauthorized (self, msg = 'Unauthorized'):
|
||||||
|
""" Called on OAuth2 failure. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CloudStorage (object):
|
||||||
|
""" Base class for uploads to cloud storage providers.
|
||||||
|
|
||||||
|
:param name: The name of the cloud service, eg. 'Dropbox'.
|
||||||
|
:param session_class: The class to use for the oauth session.
|
||||||
|
:param user_agent: The user agent to make requests to www.gutenberg.org.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = None
|
||||||
|
session_class = CloudOAuth2Session
|
||||||
|
user_agent = None
|
||||||
|
upload_endpoint = None
|
||||||
|
re_filename = re.compile (r'[/\<>:"|?*]')
|
||||||
|
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
self.host = cherrypy.config['host']
|
||||||
|
self.urlgen = routes.URLGenerator (cherrypy.routes_mapper, {'HTTP_HOST': self.host})
|
||||||
|
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output the page. """
|
||||||
|
|
||||||
|
#
|
||||||
|
# OAuth 2.0 flow see:
|
||||||
|
# http://tools.ietf.org/html/rfc6749
|
||||||
|
#
|
||||||
|
|
||||||
|
session = self.get_or_create_session ()
|
||||||
|
if 'id' in kwargs:
|
||||||
|
session.ebook = EbookMetaData (kwargs)
|
||||||
|
if session.ebook is None:
|
||||||
|
raise cherrypy.HTTPError (400, "No ebook selected. Are your cookies enabled?")
|
||||||
|
|
||||||
|
name = self.name
|
||||||
|
|
||||||
|
if 'not_approved' in kwargs or 'error' in kwargs:
|
||||||
|
self._dialog (
|
||||||
|
_('Sorry. The file could not be sent to {name}.').format (name = name),
|
||||||
|
_('Error'))
|
||||||
|
self.redirect_done (session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.oauth_dance (kwargs)
|
||||||
|
log ("Sending file %s to %s" % (
|
||||||
|
session.ebook.get_source_url (), name))
|
||||||
|
|
||||||
|
with closing (self.request_ebook (session)) as r:
|
||||||
|
r.raise_for_status ()
|
||||||
|
self.upload_file (session, r)
|
||||||
|
|
||||||
|
log ("File %s sent to %s" % (
|
||||||
|
session.ebook.get_source_url (), name))
|
||||||
|
self._dialog (
|
||||||
|
_('The file has been sent to {name}.').format (name = name),
|
||||||
|
_('Sent to {name}').format (name = name))
|
||||||
|
self.redirect_done (session)
|
||||||
|
|
||||||
|
except (OAuth2Error, ) as what:
|
||||||
|
session.unauthorized (what)
|
||||||
|
self.unauthorized ('OAuthError: ' + str (what.urlencoded))
|
||||||
|
|
||||||
|
except (RequestException, IOError, ValueError) as what:
|
||||||
|
session.unauthorized (what)
|
||||||
|
self.unauthorized ('RequestError: ' + str (what))
|
||||||
|
raise cherrypy.HTTPError (500, str (what))
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file (self, oauth_session, response):
|
||||||
|
""" Upload the file. """
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_session (self):
|
||||||
|
""" Retrieve an ongoing cloud session or create a new one. """
|
||||||
|
|
||||||
|
session_name = self.session_class.name_prefix + '_session'
|
||||||
|
session = cherrypy.session.get (session_name, self.session_class ())
|
||||||
|
cherrypy.session[session_name] = session
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def delete_session (self):
|
||||||
|
""" Delete cloud session. """
|
||||||
|
|
||||||
|
session_name = self.session_class.name_prefix + '_session'
|
||||||
|
# cherrypy.session[session_name].close ()
|
||||||
|
del cherrypy.session[session_name]
|
||||||
|
|
||||||
|
|
||||||
|
def request_ebook (self, session):
|
||||||
|
""" Return an open request object for the ebook file. """
|
||||||
|
|
||||||
|
url = session.ebook.get_source_url ()
|
||||||
|
# Caveat: use requests.get, not session.get, because it is an insecure
|
||||||
|
# transport. session.get would raise InsecureTransportError
|
||||||
|
return requests.get (
|
||||||
|
url, headers = { 'user-agent': self.user_agent }, stream = True)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_filename (self, filename):
|
||||||
|
""" Replace characters unsupported by many OSs. """
|
||||||
|
return self.re_filename.sub ('_', filename)
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_done (self, session):
|
||||||
|
""" Redirect user back to bibrec page. """
|
||||||
|
raise cherrypy.HTTPRedirect (self.urlgen (
|
||||||
|
'bibrec', id = session.ebook.id, host = self.host))
|
||||||
|
|
||||||
|
|
||||||
|
def unauthorized (self, msg = 'Unauthorized'):
|
||||||
|
""" Call on OAuth failure. """
|
||||||
|
msg = str (msg) # msg may be exception class
|
||||||
|
error_log (msg)
|
||||||
|
self.delete_session ()
|
||||||
|
raise cherrypy.HTTPError (401, msg)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _dialog (message, title):
|
||||||
|
""" Open a user-visible dialog on the next page. """
|
||||||
|
cherrypy.session['user_dialog'] = (message, title)
|
||||||
|
|
||||||
|
|
||||||
|
class EbookMetaData (object):
|
||||||
|
""" Helper class that holds ebook metadata. """
|
||||||
|
|
||||||
|
accepted_filetypes = (
|
||||||
|
'epub.images',
|
||||||
|
'epub.noimages',
|
||||||
|
'kindle.images',
|
||||||
|
'kindle.noimages',
|
||||||
|
'pdf')
|
||||||
|
|
||||||
|
|
||||||
|
def __init__ (self, kwargs):
|
||||||
|
self.id = None
|
||||||
|
self.filetype = None
|
||||||
|
|
||||||
|
try :
|
||||||
|
self.id = int (kwargs['id'])
|
||||||
|
self.filetype = kwargs['filetype']
|
||||||
|
if self.filetype not in self.accepted_filetypes:
|
||||||
|
self.filetype = None
|
||||||
|
raise ValueError
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Invalid parameters')
|
||||||
|
|
||||||
|
|
||||||
|
def get_dc (self):
|
||||||
|
""" Get a DublinCore struct for the ebook. """
|
||||||
|
dc = BaseSearcher.DC (cherrypy.engine.pool)
|
||||||
|
dc.load_from_database (self.id)
|
||||||
|
# dc.translate ()
|
||||||
|
return dc
|
||||||
|
|
||||||
|
|
||||||
|
def get_extension (self):
|
||||||
|
""" Get the ebook filename extension. """
|
||||||
|
ext = self.filetype.split ('.', 1)[0]
|
||||||
|
if ext == 'kindle':
|
||||||
|
ext = 'mobi'
|
||||||
|
return ext
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename (self):
|
||||||
|
""" Get a suitable filename to store the ebook. """
|
||||||
|
filename = self.get_dc ().make_pretty_title () + '.' + self.get_extension ()
|
||||||
|
return filename.replace (':', '_')
|
||||||
|
|
||||||
|
|
||||||
|
def get_source_url (self):
|
||||||
|
""" Return the url of the ebook file on gutenberg.org. """
|
||||||
|
|
||||||
|
return urllib.parse.urljoin (
|
||||||
|
'http://' + cherrypy.config['file_host'],
|
||||||
|
'ebooks/%d.%s' % (self.id, self.filetype))
|
|
@ -0,0 +1,101 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
ConnectionPool.py
|
||||||
|
|
||||||
|
Copyright 2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
import sqlalchemy.pool as pool
|
||||||
|
import cherrypy
|
||||||
|
from cherrypy.process import plugins
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionCreator ():
|
||||||
|
""" Creates connections for the connection pool. """
|
||||||
|
|
||||||
|
def __init__ (self, params):
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def __call__ (self):
|
||||||
|
cherrypy.log (
|
||||||
|
"Connecting to database '%(database)s' on '%(host)s:%(port)d' as user '%(user)s'."
|
||||||
|
% self.params, context = 'POSTGRES', severity = logging.INFO)
|
||||||
|
conn = psycopg2.connect (**self.params)
|
||||||
|
conn.cursor ().execute ('SET statement_timeout = 5000')
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionPool (plugins.SimplePlugin):
|
||||||
|
"""A WSPBus plugin that controls a SQLAlchemy engine/connection pool."""
|
||||||
|
|
||||||
|
def __init__ (self, bus, params = None):
|
||||||
|
plugins.SimplePlugin.__init__ (self, bus)
|
||||||
|
self.params = params
|
||||||
|
self.name = 'sqlalchemy'
|
||||||
|
self.pool = None
|
||||||
|
|
||||||
|
|
||||||
|
def _start (self):
|
||||||
|
""" Init the connection pool. """
|
||||||
|
|
||||||
|
pool_size = cherrypy.config.get ('sqlalchemy.pool_size', 5)
|
||||||
|
max_overflow = cherrypy.config.get ('sqlalchemy.max_overflow', 10)
|
||||||
|
timeout = cherrypy.config.get ('sqlalchemy.timeout', 30)
|
||||||
|
recycle = cherrypy.config.get ('sqlalchemy.recycle', 3600)
|
||||||
|
|
||||||
|
self.bus.log ("... pool_size = %d, max_overflow = %d" % (pool_size, max_overflow))
|
||||||
|
|
||||||
|
return pool.QueuePool (ConnectionCreator (self.params),
|
||||||
|
pool_size = pool_size,
|
||||||
|
max_overflow = max_overflow,
|
||||||
|
timeout = timeout,
|
||||||
|
recycle = recycle,
|
||||||
|
use_threadlocal = True)
|
||||||
|
|
||||||
|
|
||||||
|
def connect (self):
|
||||||
|
""" Return a connection. """
|
||||||
|
|
||||||
|
return self.pool.connect ()
|
||||||
|
|
||||||
|
|
||||||
|
def start (self):
|
||||||
|
""" Called on engine start. """
|
||||||
|
|
||||||
|
if self.pool is None:
|
||||||
|
self.bus.log ("Creating the SQL connection pool ...")
|
||||||
|
self.pool = self._start ()
|
||||||
|
else:
|
||||||
|
self.bus.log ("An SQL connection pool already exists.")
|
||||||
|
# start.priority = 80
|
||||||
|
|
||||||
|
|
||||||
|
def stop (self):
|
||||||
|
""" Called on engine stop. """
|
||||||
|
|
||||||
|
if self.pool is not None:
|
||||||
|
self.bus.log ("Disposing the SQL connection pool.")
|
||||||
|
self.pool.dispose ()
|
||||||
|
self.pool = None
|
||||||
|
|
||||||
|
|
||||||
|
def graceful (self):
|
||||||
|
""" Called on engine restart. """
|
||||||
|
|
||||||
|
if self.pool is not None:
|
||||||
|
self.bus.log ("Restarting the SQL connection pool ...")
|
||||||
|
self.pool.dispose ()
|
||||||
|
self.pool = self._start ()
|
||||||
|
|
||||||
|
|
||||||
|
cherrypy.process.plugins.ConnectionPool = ConnectionPool
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
CoverPages.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Serve cover images of most popular and latest ebooks.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import six
|
||||||
|
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
class CoverPages (object):
|
||||||
|
""" Output a gallery of cover pages. """
|
||||||
|
|
||||||
|
orders = { 'latest': 'release_date',
|
||||||
|
'popular': 'downloads' }
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def serve (rows, size):
|
||||||
|
""" Output a gallery of coverpages. """
|
||||||
|
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
cherrypy.response.headers['Content-Language'] = 'en'
|
||||||
|
|
||||||
|
s = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" xml:base="http://www.gutenberg.org">
|
||||||
|
<head>
|
||||||
|
<title>Cover Flow</title>
|
||||||
|
<style>
|
||||||
|
.cover-thumb {
|
||||||
|
display: inline-block;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.cover-thumb-small {
|
||||||
|
width: 76px;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
.cover-thumb-medium {
|
||||||
|
width: 210px;
|
||||||
|
height: 310px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body><div>"""
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
url = '/' + row.filename
|
||||||
|
href = '/ebooks/%d' % row.pk
|
||||||
|
title = gg.xmlspecialchars (row.title)
|
||||||
|
title = title.replace ('"', '"')
|
||||||
|
|
||||||
|
s += """<a href="{href}"
|
||||||
|
title="{title}"
|
||||||
|
class="cover-thumb cover-thumb-{size}" target="_top"
|
||||||
|
style="background-image: url({url})"> </a>\n""".format (
|
||||||
|
url = url, href = href, title = title, size = size)
|
||||||
|
|
||||||
|
return (s + '</div></body></html>\n').encode ('utf-8')
|
||||||
|
|
||||||
|
def index (self, count, size, order, **kwargs):
|
||||||
|
""" Internal help function. """
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = int (count)
|
||||||
|
if count < 1:
|
||||||
|
raise ValueError ('count < 0')
|
||||||
|
if size not in ('small', 'medium'):
|
||||||
|
raise ValueError ('bogus size')
|
||||||
|
order = 'books.%s' % self.orders[order]
|
||||||
|
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (
|
||||||
|
"""SELECT files.filename, books.pk, books.title FROM files, books
|
||||||
|
WHERE files.fk_books = books.pk
|
||||||
|
AND files.diskstatus = 0
|
||||||
|
AND files.fk_filetypes = %%(size)s
|
||||||
|
ORDER BY %s DESC
|
||||||
|
OFFSET 1 LIMIT %%(count)s -- %s""" % (order, cherrypy.request.remote.ip),
|
||||||
|
{ 'count': count,
|
||||||
|
'size': 'cover.%s' % size,
|
||||||
|
})
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
return self.serve (rows, size)
|
||||||
|
|
||||||
|
except (ValueError, KeyError) as what:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. %s' % six.text_type (what))
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
raise cherrypy.HTTPError (500, 'Internal Server Error.')
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Dropbox.py
|
||||||
|
|
||||||
|
Copyright 2012-17 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The send-to-dropbox pages.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import CloudStorage
|
||||||
|
|
||||||
|
|
||||||
|
class DropboxOAuth2Session (CloudStorage.CloudOAuth2Session):
|
||||||
|
""" Hold parameters for OAuth2. """
|
||||||
|
|
||||||
|
name_prefix = 'dropbox'
|
||||||
|
oauth2_auth_endpoint = 'https://www.dropbox.com/oauth2/authorize'
|
||||||
|
oauth2_token_endpoint = 'https://api.dropbox.com/oauth2/token'
|
||||||
|
oauth2_scope = None
|
||||||
|
|
||||||
|
|
||||||
|
class Dropbox (CloudStorage.CloudStorage):
|
||||||
|
""" Send files to dropbox using OAuth2 authentication. """
|
||||||
|
|
||||||
|
name = 'Dropbox'
|
||||||
|
session_class = DropboxOAuth2Session
|
||||||
|
user_agent = 'PG2Dropbox/0.3'
|
||||||
|
upload_endpoint = 'https://content.dropboxapi.com/2/files/upload'
|
||||||
|
|
||||||
|
# Incompatible characters see: https://www.dropbox.com/help/145/en
|
||||||
|
# also added ' and ,
|
||||||
|
re_filename = re.compile ('[/\\<>:"|?*\',]')
|
||||||
|
|
||||||
|
def upload_file (self, session, response):
|
||||||
|
""" Get the file from gutenberg.org and upload it to dropbox.
|
||||||
|
:param session: authorized OAuthlib session.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
'path': '/' + self.fix_filename (session.ebook.get_filename ())
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'Authorization' : 'Bearer ' + str (session.token),
|
||||||
|
'Content-Type' : 'application/octet-stream',
|
||||||
|
'Dropbox-API-Arg' : json.dumps (parameters)
|
||||||
|
}
|
||||||
|
with closing (session.post (self.upload_endpoint,
|
||||||
|
data = response.content,
|
||||||
|
headers = headers)) as r:
|
||||||
|
CloudStorage.error_log (r.text)
|
||||||
|
r.raise_for_status ()
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
DublinCoreI18n.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Translate a DublinCore struct with Babel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import babel
|
||||||
|
|
||||||
|
|
||||||
|
class DublinCoreI18nMixin (object):
|
||||||
|
""" Translator Mixin for GutenbergDatabaseDublinCore class. """
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
self.translated = False
|
||||||
|
self.hr_release_date = None
|
||||||
|
self.rights = None
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dummy_text_holder ():
|
||||||
|
"""Never gets called.
|
||||||
|
|
||||||
|
Only holds some gettext messages to translate. Keep this in
|
||||||
|
sync with GutenbergDatabaseDublinCore.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_('Copyrighted. Read the copyright notice inside this book for details.')
|
||||||
|
_('Public domain in the USA.')
|
||||||
|
|
||||||
|
|
||||||
|
def translate (self):
|
||||||
|
""" Translate DublinCore struct. """
|
||||||
|
|
||||||
|
if self.translated:
|
||||||
|
# already translated
|
||||||
|
return
|
||||||
|
|
||||||
|
self.hr_release_date = babel.dates.format_date (
|
||||||
|
self.release_date, locale = str (cherrypy.response.i18n.locale))
|
||||||
|
|
||||||
|
if cherrypy.response.i18n.locale.language == 'en':
|
||||||
|
# no translation required
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rights = _(self.rights)
|
||||||
|
for author in self.authors:
|
||||||
|
author.role = _(author.role)
|
||||||
|
for marc in self.marcs:
|
||||||
|
marc.caption = _(marc.caption)
|
||||||
|
for dcmitype in self.dcmitypes:
|
||||||
|
dcmitype.description = _(dcmitype.description)
|
||||||
|
for lang in self.languages:
|
||||||
|
if lang.id in cherrypy.response.i18n.locale.languages:
|
||||||
|
lang.language = cherrypy.response.i18n.locale.languages[lang.id].capitalize ()
|
||||||
|
for file_ in self.files:
|
||||||
|
file_.hr_filetype = _(file_.hr_filetype)
|
||||||
|
for file_ in self.generated_files:
|
||||||
|
file_.hr_filetype = _(file_.hr_filetype)
|
||||||
|
|
||||||
|
self.translated = True
|
|
@ -0,0 +1,79 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formatters.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Formatters for all mediatypes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import genshi.template
|
||||||
|
import genshi.filters
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
import HTMLFormatter
|
||||||
|
import OPDSFormatter
|
||||||
|
import JSONFormatter
|
||||||
|
|
||||||
|
def format (format_, page, os_):
|
||||||
|
""" Main entry point. """
|
||||||
|
|
||||||
|
return formatters[format_].format (page, os_)
|
||||||
|
|
||||||
|
|
||||||
|
formatters = {}
|
||||||
|
formatters['opds'] = OPDSFormatter.OPDSFormatter ()
|
||||||
|
formatters['stanza'] = formatters['opds']
|
||||||
|
formatters['mobile'] = HTMLFormatter.MobileFormatter ()
|
||||||
|
formatters['html'] = HTMLFormatter.HTMLFormatter ()
|
||||||
|
formatters['json'] = JSONFormatter.JSONFormatter ()
|
||||||
|
# FIXME: only needed to load sitemap.xml templates
|
||||||
|
formatters['xml'] = HTMLFormatter.XMLishFormatter ()
|
||||||
|
|
||||||
|
def on_template_loaded (template):
|
||||||
|
"""
|
||||||
|
Callback.
|
||||||
|
|
||||||
|
We need to use the callback because we are using includes.
|
||||||
|
The callback will also setup () the includes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
genshi.filters.Translator (_).setup (template)
|
||||||
|
|
||||||
|
|
||||||
|
def init ():
|
||||||
|
""" Load all template files in template_dir. """
|
||||||
|
|
||||||
|
template_dir = cherrypy.config['genshi.template_dir']
|
||||||
|
|
||||||
|
for fn in glob.glob (os.path.join (template_dir, '*')):
|
||||||
|
if fn.endswith ('~') or fn.endswith ('#') or fn.endswith ('schemas.xml'):
|
||||||
|
# backup file or emacs temp file
|
||||||
|
continue
|
||||||
|
|
||||||
|
cherrypy.engine.autoreload.files.update (fn)
|
||||||
|
|
||||||
|
bn = os.path.basename (fn)
|
||||||
|
template = genshi.template.TemplateLoader (
|
||||||
|
template_dir,
|
||||||
|
callback = on_template_loaded).load (bn)
|
||||||
|
|
||||||
|
page, dot_format = os.path.splitext (bn)
|
||||||
|
|
||||||
|
formatters[dot_format[1:]].set_template (page, template)
|
||||||
|
|
||||||
|
cherrypy.log ("Template '%s' loaded." % fn,
|
||||||
|
context = 'GENSHI', severity = logging.INFO)
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
GDrive.py
|
||||||
|
|
||||||
|
Copyright 2013-15 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The send-to-google-drive pages.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
import json
|
||||||
|
|
||||||
|
import CloudStorage
|
||||||
|
|
||||||
|
|
||||||
|
class GDriveSession (CloudStorage.CloudOAuth2Session):
|
||||||
|
""" Hold parameters for OAuth. """
|
||||||
|
|
||||||
|
#
|
||||||
|
# OAuth 2.0 flow see:
|
||||||
|
# http://tools.ietf.org/html/rfc6749
|
||||||
|
# https://developers.google.com/api-client-library/python/guide/aaa_oauth
|
||||||
|
#
|
||||||
|
|
||||||
|
name_prefix = 'gdrive'
|
||||||
|
oauth2_auth_endpoint = 'https://accounts.google.com/o/oauth2/auth'
|
||||||
|
oauth2_token_endpoint = 'https://accounts.google.com/o/oauth2/token'
|
||||||
|
# Check https://developers.google.com/drive/web/scopes for all available scopes
|
||||||
|
oauth2_scope = 'https://www.googleapis.com/auth/drive.file'
|
||||||
|
|
||||||
|
|
||||||
|
class GDrive (CloudStorage.CloudStorage):
|
||||||
|
""" Send files to Google Drive. """
|
||||||
|
|
||||||
|
name = 'Google Drive'
|
||||||
|
session_class = GDriveSession
|
||||||
|
user_agent = 'PG2GDrive/0.2'
|
||||||
|
upload_endpoint = 'https://www.googleapis.com/upload/drive/v2/files?uploadType=resumable'
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file (self, session, request):
|
||||||
|
""" Upload a file to google drive. """
|
||||||
|
|
||||||
|
file_metadata = {
|
||||||
|
'title': self.fix_filename (session.ebook.get_filename ()),
|
||||||
|
'description': 'A Project Gutenberg Ebook',
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'X-Upload-Content-Type': request.headers['Content-Type'],
|
||||||
|
'X-Upload-Content-Length': request.headers['Content-Length'],
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
}
|
||||||
|
with closing (session.post (self.upload_endpoint,
|
||||||
|
data = json.dumps (file_metadata),
|
||||||
|
headers = headers)) as r2:
|
||||||
|
r2.raise_for_status ()
|
||||||
|
session_uri = r2.headers['Location']
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': request.headers['Content-Type'],
|
||||||
|
}
|
||||||
|
with closing (session.put (session_uri,
|
||||||
|
data = request.iter_content (1024 * 1024),
|
||||||
|
headers = headers)) as r3:
|
||||||
|
r3.raise_for_status ()
|
|
@ -0,0 +1,208 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
HTMLFormatter.py
|
||||||
|
|
||||||
|
Copyright 2009-2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Produce a HTML page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import operator
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import genshi.output
|
||||||
|
import six
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import BaseFormatter
|
||||||
|
|
||||||
|
# filetypes ignored on desktop site
|
||||||
|
NO_DESKTOP_FILETYPES = 'plucker qioo rdf rst rst.gen rst.master tei cover.medium cover.small'.split ()
|
||||||
|
|
||||||
|
# filetypes shown on mobile site
|
||||||
|
MOBILE_TYPES = (mt.epub, mt.plucker, mt.mobi, mt.pdf, 'text/html', mt.html, mt.qioo)
|
||||||
|
|
||||||
|
# filetypes which are usually handed over to a separate app on mobile devices
|
||||||
|
HANDOVER_TYPES = (mt.epub, mt.plucker, mt.mobi, mt.pdf, mt.qioo)
|
||||||
|
|
||||||
|
# self-contained files we can send to dropbox
|
||||||
|
CLOUD_TYPES = (mt.epub, mt.mobi, mt.pdf)
|
||||||
|
|
||||||
|
class XMLishFormatter (BaseFormatter.BaseFormatter):
|
||||||
|
""" Produce XMLish output. """
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
super (XMLishFormatter, self).__init__ ()
|
||||||
|
|
||||||
|
|
||||||
|
def fix_dc (self, dc, os):
|
||||||
|
""" Tweak dc. """
|
||||||
|
|
||||||
|
super (XMLishFormatter, self).fix_dc (dc, os)
|
||||||
|
|
||||||
|
for file_ in dc.generated_files:
|
||||||
|
file_.help_topic = file_.hr_filetype
|
||||||
|
file_.compression = 'none'
|
||||||
|
file_.encoding = None
|
||||||
|
|
||||||
|
for file_ in dc.files + dc.generated_files:
|
||||||
|
type_ = six.text_type (file_.mediatypes[0])
|
||||||
|
m = type_.partition (';')[0]
|
||||||
|
if m in CLOUD_TYPES:
|
||||||
|
file_.dropbox_url = os.url (
|
||||||
|
'dropbox_send', id = dc.project_gutenberg_id, filetype = file_.filetype)
|
||||||
|
file_.gdrive_url = os.url (
|
||||||
|
'gdrive_send', id = dc.project_gutenberg_id, filetype = file_.filetype)
|
||||||
|
file_.msdrive_url = os.url (
|
||||||
|
'msdrive_send', id = dc.project_gutenberg_id, filetype = file_.filetype)
|
||||||
|
|
||||||
|
if m in HANDOVER_TYPES:
|
||||||
|
file_.url = file_.url + '?' + urllib.parse.urlencode (
|
||||||
|
{ 'session_id': str (cherrypy.session.id) } )
|
||||||
|
|
||||||
|
for file_ in dc.files:
|
||||||
|
file_.honeypot_url = os.url (
|
||||||
|
'honeypot_send', id = dc.project_gutenberg_id, filetype = file_.filetype)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def format (self, page, os):
|
||||||
|
""" Format to HTML. """
|
||||||
|
|
||||||
|
for e in os.entries:
|
||||||
|
if isinstance (e, BaseSearcher.DC):
|
||||||
|
self.fix_dc (e, os)
|
||||||
|
|
||||||
|
# loop again because fix:dc appends things
|
||||||
|
for e in os.entries:
|
||||||
|
if isinstance (e, BaseSearcher.Cat):
|
||||||
|
if e.url:
|
||||||
|
e.icon2 = e.icon2 or 'next'
|
||||||
|
else:
|
||||||
|
e.class_ += 'grayed'
|
||||||
|
|
||||||
|
if os.title_icon:
|
||||||
|
os.class_ += 'icon_' + os.title_icon
|
||||||
|
|
||||||
|
os.entries.sort (key = operator.attrgetter ('order'))
|
||||||
|
|
||||||
|
return self.render (page, os)
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLFormatter (XMLishFormatter):
|
||||||
|
""" Produce HTML output. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = 'text/html; charset=UTF-8'
|
||||||
|
DOCTYPE = ('html',
|
||||||
|
'-//W3C//DTD XHTML+RDFa 1.0//EN',
|
||||||
|
'http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd')
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
super (HTMLFormatter, self).__init__ ()
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
# return BaseFormatter.XHTMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
||||||
|
return genshi.output.XHTMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_dc (self, dc, os):
|
||||||
|
""" Add some info to dc for easier templating.
|
||||||
|
|
||||||
|
Also make sure that dc `walks like a cat´. """
|
||||||
|
|
||||||
|
super (HTMLFormatter, self).fix_dc (dc, os)
|
||||||
|
|
||||||
|
#for author in dc.authors:
|
||||||
|
# author.authors_page_url = (
|
||||||
|
# "/browse/authors/%s#a%d" % (author.name[:1].lower (), author.id))
|
||||||
|
if dc.new_filesystem:
|
||||||
|
dc.base_dir = "/files/%d/" % dc.project_gutenberg_id
|
||||||
|
# dc.mirror_dir = gg.archive_dir (dc.project_gutenberg_id)
|
||||||
|
else:
|
||||||
|
dc.base_dir = None
|
||||||
|
# dc.mirror_dir = None
|
||||||
|
|
||||||
|
dc.magnetlink = None
|
||||||
|
|
||||||
|
# hide all txt files but the first one
|
||||||
|
txtcount = showncount = 0
|
||||||
|
|
||||||
|
for file_ in dc.files + dc.generated_files:
|
||||||
|
filetype = file_.filetype or ''
|
||||||
|
file_.hidden = False
|
||||||
|
|
||||||
|
if filetype in NO_DESKTOP_FILETYPES:
|
||||||
|
file_.hidden = True
|
||||||
|
if file_.compression != 'none':
|
||||||
|
file_.hidden = True
|
||||||
|
if filetype.startswith ('txt'):
|
||||||
|
if txtcount > 0:
|
||||||
|
file_.hidden = True
|
||||||
|
txtcount += 1
|
||||||
|
if filetype != 'txt':
|
||||||
|
file_.encoding = None
|
||||||
|
if file_.encoding:
|
||||||
|
file_.hr_filetype += ' ' + file_.encoding.upper ()
|
||||||
|
if filetype.startswith ('html') and file_.compression == 'none':
|
||||||
|
file_.hr_filetype = 'Read this book online: {}'.format (file_.hr_filetype)
|
||||||
|
if not file_.hidden:
|
||||||
|
showncount += 1
|
||||||
|
|
||||||
|
# if we happened to hide everything, show all files
|
||||||
|
if showncount == 0:
|
||||||
|
for file_ in dc.files + dc.generated_files:
|
||||||
|
file_.hidden = False
|
||||||
|
|
||||||
|
|
||||||
|
class MobileFormatter (XMLishFormatter):
|
||||||
|
""" Produce HTML output suitable for mobile devices. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = mt.xhtml + '; charset=UTF-8'
|
||||||
|
DOCTYPE = ('html',
|
||||||
|
'-//WAPFORUM//DTD XHTML Mobile 1.2//EN',
|
||||||
|
'http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd')
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
super (MobileFormatter, self).__init__ ()
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
return genshi.output.XMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_dc (self, dc, os):
|
||||||
|
""" Add some info to dc for easier templating.
|
||||||
|
|
||||||
|
Also make sure that dc `walks like a cat´. """
|
||||||
|
|
||||||
|
super (MobileFormatter, self).fix_dc (dc, os)
|
||||||
|
|
||||||
|
for file_ in dc.files + dc.generated_files:
|
||||||
|
if len (file_.mediatypes) == 1:
|
||||||
|
type_ = six.text_type (file_.mediatypes[0])
|
||||||
|
m = type_.partition (';')[0]
|
||||||
|
if m in MOBILE_TYPES:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.type = file_.mediatypes[0]
|
||||||
|
cat.header = _('Download')
|
||||||
|
cat.title = file_.hr_filetype
|
||||||
|
cat.extra = file_.hr_extent
|
||||||
|
|
||||||
|
cat.charset = file_.encoding
|
||||||
|
cat.url = file_.url
|
||||||
|
cat.icon = dc.icon
|
||||||
|
cat.icon2 = 'download'
|
||||||
|
cat.class_ += 'filelink'
|
||||||
|
cat.order = 20
|
||||||
|
os.entries.append (cat)
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Icons.py
|
||||||
|
|
||||||
|
Copyright 2010-2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Stock icons.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# This list is used to generate a sprite.
|
||||||
|
|
||||||
|
THUMBS = {
|
||||||
|
'alias' : 'pics/24/messagebox_info.png',
|
||||||
|
'alpha' : 'pics/24/applixware.png',
|
||||||
|
'audiobook' : 'pics/24/amarok.png',
|
||||||
|
'author' : 'pics/24/kuser.png',
|
||||||
|
'back' : 'pics/24/previous.png',
|
||||||
|
'bibrec' : 'pics/24/messagebox_info.png',
|
||||||
|
'bookshelf' : 'pics/24/bookcase.png',
|
||||||
|
'date' : 'pics/24/date.png',
|
||||||
|
'download' : 'pics/24/dock.png',
|
||||||
|
'external' : 'pics/24/browser.png',
|
||||||
|
'folder' : 'pics/24/folder_yellow_open.png',
|
||||||
|
'home' : 'pics/24/gohome.png',
|
||||||
|
'internal' : 'pics/24/pg-logo.png',
|
||||||
|
'language' : 'pics/24/locale.png',
|
||||||
|
'more' : 'pics/24/edit_add.png',
|
||||||
|
'popular' : 'pics/24/bookmark.png',
|
||||||
|
'print' : 'pics/24/print.png',
|
||||||
|
'quantity' : 'pics/24/licq.png',
|
||||||
|
'random' : 'pics/24/roll.png',
|
||||||
|
'rss' : 'pics/24/rss.png',
|
||||||
|
'search' : 'pics/24/viewmag.png',
|
||||||
|
'subject' : 'pics/24/edu_science.png',
|
||||||
|
'suggestion' : 'pics/24/ktip.png',
|
||||||
|
'book' : 'pics/24/kdict.png',
|
||||||
|
'book_no_images' : 'pics/24/book-cut.png',
|
||||||
|
|
||||||
|
'cancel' : 'pics/16/no.png',
|
||||||
|
'help' : 'pics/16/help.png',
|
||||||
|
'next' : 'pics/16/next.png',
|
||||||
|
|
||||||
|
'external_link' : 'pics/link.png',
|
||||||
|
'qrtab' : 'pics/qricon.png',
|
||||||
|
'smsearch' : 'pics/16/viewmag.png',
|
||||||
|
|
||||||
|
'logo' : 'pics/android-touch-icon.png',
|
||||||
|
'logo_yellow' : 'pics/android-touch-icon-yellow.png',
|
||||||
|
'paypal' : 'pics/paypal-donate-button-LG.gif',
|
||||||
|
'flattr' : 'pics/flattr-badge-large.png',
|
||||||
|
'facebook' : 'pics/facebook-20x20.png',
|
||||||
|
'dropbox' : 'pics/dropbox-22x22.png',
|
||||||
|
'gdrive' : 'pics/gdrive-24x21.png',
|
||||||
|
'msdrive' : 'pics/msdrive-24x24.png',
|
||||||
|
'twitter' : 'pics/twitter_whiteonblue-20x20.png',
|
||||||
|
'gplus' : 'pics/gplus-20x20.png',
|
||||||
|
|
||||||
|
# 'hosted' : 'pics/ibiblio-hosted-110x32.png',
|
||||||
|
# 'apache' : 'pics/apache-powered-118x41.gif',
|
||||||
|
# 'python' : 'pics/python-powered-w-81x32.png',
|
||||||
|
# 'postgres' : 'pics/postgres-powered-88x31.gif',
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
JSONFormatter.py
|
||||||
|
|
||||||
|
Copyright 2009-2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Produce a JSON response.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
|
||||||
|
import BaseFormatter
|
||||||
|
|
||||||
|
RE_WORD = re.compile (r'\W+', re.U)
|
||||||
|
|
||||||
|
class JSONFormatter (BaseFormatter.BaseFormatter):
|
||||||
|
""" Produce JSON output. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = mt.json + '; charset=UTF-8'
|
||||||
|
CONTENT_TYPE = 'application/json; charset=UTF-8'
|
||||||
|
DOCTYPE = None
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
super (JSONFormatter, self).__init__ ()
|
||||||
|
|
||||||
|
|
||||||
|
def format (self, dummy_page, os):
|
||||||
|
|
||||||
|
# Specs:
|
||||||
|
# http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
|
||||||
|
|
||||||
|
sugg0 = []
|
||||||
|
sugg1 = []
|
||||||
|
sugg2 = []
|
||||||
|
|
||||||
|
for e in os.entries:
|
||||||
|
if 'navlink' not in e.class_:
|
||||||
|
sugg0.append (e.title)
|
||||||
|
sugg1.append (e.subtitle)
|
||||||
|
sugg2.append (e.url)
|
||||||
|
|
||||||
|
self.send_headers ()
|
||||||
|
return json.dumps ( [os.query, sugg0, sugg1, sugg2] ).encode ('utf-8')
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
MSDrive.py
|
||||||
|
|
||||||
|
Copyright 2014,15 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The send-to-microsoft-drive pages.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
import CloudStorage
|
||||||
|
|
||||||
|
|
||||||
|
class MSDriveSession (CloudStorage.CloudOAuth2Session):
|
||||||
|
""" Hold parameters for OAuth. """
|
||||||
|
|
||||||
|
#
|
||||||
|
# OAuth 2.0 flow see:
|
||||||
|
# http://tools.ietf.org/html/rfc6749
|
||||||
|
# http://msdn.microsoft.com/en-us/library/live/hh243649
|
||||||
|
#
|
||||||
|
|
||||||
|
name_prefix = 'msdrive'
|
||||||
|
oauth2_auth_endpoint = 'https://login.live.com/oauth20_authorize.srf'
|
||||||
|
oauth2_token_endpoint = 'https://login.live.com/oauth20_token.srf'
|
||||||
|
oauth2_scope = 'wl.signin wl.basic wl.skydrive wl.skydrive_update'
|
||||||
|
|
||||||
|
|
||||||
|
class MSDrive (CloudStorage.CloudStorage):
|
||||||
|
""" Send files to Microsoft Drive. """
|
||||||
|
|
||||||
|
name = 'OneDrive'
|
||||||
|
session_class = MSDriveSession
|
||||||
|
user_agent = 'PG2MSDrive/0.2'
|
||||||
|
upload_endpoint = 'https://apis.live.net/v5.0/me/skydrive/files/'
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file (self, session, request):
|
||||||
|
""" Upload a file to microsoft drive. """
|
||||||
|
|
||||||
|
url = self.upload_endpoint + self.fix_filename (session.ebook.get_filename ())
|
||||||
|
|
||||||
|
# MSDrive does not like such never-heard-of-before
|
||||||
|
# content-types like 'epub', so we just send it without
|
||||||
|
# content-type.
|
||||||
|
with closing (session.put (url, data = request.iter_content (1024 * 1024))) as r:
|
||||||
|
r.raise_for_status ()
|
|
@ -0,0 +1,141 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
MyRamSession.py
|
||||||
|
|
||||||
|
Copyright 2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
A quick Python3 fix for the cherrypy RamSession. May be removed when
|
||||||
|
RamSession is fixed upstream.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
import MyRamSession
|
||||||
|
cherrypy.lib.sessions.RamSession = MyRamSession.FixedRamSession
|
||||||
|
cherrypy.lib.sessions.MyramSession = MyRamSession.MyRamSession
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import cherrypy.lib.sessions
|
||||||
|
|
||||||
|
|
||||||
|
class Struct (object):
|
||||||
|
""" Data store. """
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
self.expires = None
|
||||||
|
self.data = None
|
||||||
|
self.cache_lock = threading.Lock ()
|
||||||
|
|
||||||
|
|
||||||
|
class MyRamSession (cherrypy.lib.sessions.Session):
|
||||||
|
""" A cherrypy session kept in ram. """
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
# all inserts/deletes in cache must be guarded by this lock
|
||||||
|
# or we will get 'RuntimeError: dictionary changed size during iteration'
|
||||||
|
# because you cannot atomically iterate a dict in Python3
|
||||||
|
cache_lock = threading.Lock ()
|
||||||
|
|
||||||
|
|
||||||
|
def __init__ (self, id_ = None, **kwargs):
|
||||||
|
super (MyRamSession, self).__init__ (id_, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_up (self):
|
||||||
|
"""Clean up expired sessions."""
|
||||||
|
|
||||||
|
now = self.now ()
|
||||||
|
def expired (x):
|
||||||
|
return x[1].expires <= now
|
||||||
|
|
||||||
|
with self.cache_lock:
|
||||||
|
for id_, s in list (filter (expired, self.cache.items ())):
|
||||||
|
self.cache.pop (id_, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _exists (self):
|
||||||
|
return self.id in self.cache
|
||||||
|
|
||||||
|
|
||||||
|
def _load (self):
|
||||||
|
try:
|
||||||
|
s = self.cache[self.id]
|
||||||
|
return s.data, s.expires
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _save (self, expires):
|
||||||
|
s = self.cache.get (self.id, Struct ())
|
||||||
|
s.expires = expires
|
||||||
|
s.data = self._data
|
||||||
|
with self.cache_lock:
|
||||||
|
self.cache[self.id] = s
|
||||||
|
|
||||||
|
|
||||||
|
def _delete (self):
|
||||||
|
with self.cache_lock:
|
||||||
|
self.cache.pop (self.id, None)
|
||||||
|
|
||||||
|
|
||||||
|
def acquire_lock (self):
|
||||||
|
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cache[self.id].lock.acquire ()
|
||||||
|
self.locked = True
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def release_lock (self):
|
||||||
|
"""Release the lock on the currently-loaded session data."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cache[self.id].lock.release ()
|
||||||
|
self.locked = False
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def __len__ (self):
|
||||||
|
"""Return the number of active sessions."""
|
||||||
|
|
||||||
|
return len (self.cache)
|
||||||
|
|
||||||
|
|
||||||
|
from cherrypy._cpcompat import copyitems
|
||||||
|
|
||||||
|
class FixedRamSession (cherrypy.lib.sessions.RamSession):
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
"""Clean up expired sessions."""
|
||||||
|
now = self.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for id, (data, expiration_time) in copyitems(self.cache):
|
||||||
|
if expiration_time <= now:
|
||||||
|
try:
|
||||||
|
del self.cache[id]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
del self.locks[id]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# added to remove obsolete lock objects
|
||||||
|
for id in list(self.locks):
|
||||||
|
if id not in self.cache:
|
||||||
|
self.locks.pop(id, None)
|
||||||
|
|
||||||
|
except RuntimeError:
|
||||||
|
# RuntimeError: dictionary changed size during iteration
|
||||||
|
# Do nothig. Keep cleanup thread running and maybe next time lucky.
|
||||||
|
pass
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
OPDSFormatter.py
|
||||||
|
|
||||||
|
Copyright 2009-2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Produce an OPDS feed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
|
||||||
|
import genshi.output
|
||||||
|
import cherrypy
|
||||||
|
import six
|
||||||
|
|
||||||
|
from libgutenberg.GutenbergGlobals import Struct, xmlspecialchars
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
from libgutenberg import DublinCore
|
||||||
|
|
||||||
|
import BaseFormatter
|
||||||
|
from Icons import THUMBS as th
|
||||||
|
|
||||||
|
|
||||||
|
# files a mobile can download
|
||||||
|
OPDS_TYPES = (mt.epub, mt.mobi, mt.pdf)
|
||||||
|
|
||||||
|
# domains allowed to XMLHttpRequest () our OPDS feed
|
||||||
|
CORS_DOMAINS = '*'
|
||||||
|
|
||||||
|
class OPDSFormatter (BaseFormatter.BaseFormatter):
|
||||||
|
""" Produces opds output. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = mt.opds + '; charset=UTF-8'
|
||||||
|
DOCTYPE = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
return genshi.output.XMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
||||||
|
|
||||||
|
|
||||||
|
def send_headers (self):
|
||||||
|
""" Send HTTP content-type header. """
|
||||||
|
|
||||||
|
cherrypy.response.headers['Access-Control-Allow-Origin'] = CORS_DOMAINS
|
||||||
|
super (OPDSFormatter, self).send_headers ()
|
||||||
|
|
||||||
|
|
||||||
|
def format (self, page, os):
|
||||||
|
""" Format os struct into opds output. """
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for dc in os.entries:
|
||||||
|
dc.thumbnail = None
|
||||||
|
if isinstance (dc, DublinCore.DublinCore):
|
||||||
|
dc.image_flags = 0
|
||||||
|
if dc.has_images ():
|
||||||
|
dc.pool = None
|
||||||
|
dc_copy = copy.deepcopy (dc)
|
||||||
|
|
||||||
|
dc.image_flags = 2
|
||||||
|
dc.icon = 'title_no_images'
|
||||||
|
self.fix_dc (dc, os)
|
||||||
|
entries.append (dc)
|
||||||
|
|
||||||
|
dc_copy.image_flags = 3
|
||||||
|
self.fix_dc (dc_copy, os, True)
|
||||||
|
entries.append (dc_copy)
|
||||||
|
else:
|
||||||
|
self.fix_dc (dc, os)
|
||||||
|
entries.append (dc)
|
||||||
|
else:
|
||||||
|
# actually not a dc
|
||||||
|
# throw out 'start over' link, FIXME: actually throw out all non-dc's ?
|
||||||
|
if page == 'bibrec' and dc.rel == 'start':
|
||||||
|
continue
|
||||||
|
dc.links = []
|
||||||
|
if dc.icon in th:
|
||||||
|
link = Struct ()
|
||||||
|
link.type = mt.png
|
||||||
|
link.rel = 'thumb'
|
||||||
|
link.url = self.data_url (th[dc.icon])
|
||||||
|
link.title = None
|
||||||
|
link.length = None
|
||||||
|
dc.links.append (link)
|
||||||
|
|
||||||
|
entries.append (dc)
|
||||||
|
|
||||||
|
os.entries = entries
|
||||||
|
|
||||||
|
if page == 'bibrec':
|
||||||
|
# we have just one template for both
|
||||||
|
page = 'results'
|
||||||
|
|
||||||
|
return self.render (page, os)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_dc (self, dc, os, want_images = False):
|
||||||
|
""" Make fixes to dublincore struct. """
|
||||||
|
|
||||||
|
def to_html (text):
|
||||||
|
""" Turn plain text into html. """
|
||||||
|
return re.sub (r'[\r\n]+', '<br/>', xmlspecialchars (text))
|
||||||
|
|
||||||
|
def key_role (author):
|
||||||
|
""" Sort authors first, then other contributors. """
|
||||||
|
if author.marcrel in ('cre', 'aut'):
|
||||||
|
return ''
|
||||||
|
return author.marcrel
|
||||||
|
|
||||||
|
super (OPDSFormatter, self).fix_dc (dc, os)
|
||||||
|
|
||||||
|
if dc.icon in th:
|
||||||
|
dc.thumbnail = self.data_url (th[dc.icon])
|
||||||
|
|
||||||
|
dc.links = []
|
||||||
|
|
||||||
|
dc.title_html = to_html (dc.title)
|
||||||
|
|
||||||
|
dc.authors.sort (key = key_role)
|
||||||
|
|
||||||
|
for file_ in dc.files + dc.generated_files:
|
||||||
|
if len (file_.mediatypes) == 1:
|
||||||
|
type_ = six.text_type (file_.mediatypes[0])
|
||||||
|
filetype = file_.filetype or ''
|
||||||
|
|
||||||
|
if type_.partition (';')[0] in OPDS_TYPES:
|
||||||
|
if ((filetype.find ('.images') > -1) == want_images):
|
||||||
|
link = Struct ()
|
||||||
|
link.type = type_
|
||||||
|
link.title = file_.hr_filetype
|
||||||
|
link.url = file_.url
|
||||||
|
link.length = str (file_.extent)
|
||||||
|
link.rel = 'acquisition'
|
||||||
|
dc.links.append (link)
|
||||||
|
|
||||||
|
if filetype == 'cover.small':
|
||||||
|
link = Struct ()
|
||||||
|
link.type = six.text_type (file_.mediatypes[0])
|
||||||
|
link.title = None
|
||||||
|
link.url = file_.url
|
||||||
|
link.length = None
|
||||||
|
link.rel = 'thumb'
|
||||||
|
dc.links.append (link)
|
||||||
|
|
||||||
|
if filetype == 'cover.medium':
|
||||||
|
link = Struct ()
|
||||||
|
link.type = six.text_type (file_.mediatypes[0])
|
||||||
|
link.title = None
|
||||||
|
link.url = file_.url
|
||||||
|
link.length = None
|
||||||
|
link.rel = 'cover'
|
||||||
|
dc.links.append (link)
|
|
@ -0,0 +1,305 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Page.py
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Base class for all pages.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
from libgutenberg.GutenbergDatabase import DatabaseError
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import Formatters
|
||||||
|
|
||||||
|
class Page (object):
|
||||||
|
""" Base for all pages. """
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
self.supported_book_mediatypes = [ mt.epub, mt.mobi ]
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format (os):
|
||||||
|
""" Output page. """
|
||||||
|
return Formatters.formatters[os.format].format (os.template, os)
|
||||||
|
|
||||||
|
|
||||||
|
def client_book_mediatypes (self):
|
||||||
|
""" Return the book mediatypes accepted by the client. """
|
||||||
|
client_accepted_book_mediatypes = []
|
||||||
|
|
||||||
|
accept_header = cherrypy.request.headers.get ('Accept')
|
||||||
|
|
||||||
|
if accept_header is None:
|
||||||
|
client_accepted_book_mediatypes = self.supported_book_mediatypes
|
||||||
|
else:
|
||||||
|
#cherrypy.log ("Accept: %s" % accept_header,
|
||||||
|
# context = 'REQUEST', severity = logging.DEBUG)
|
||||||
|
|
||||||
|
client_accepted_book_mediatypes = []
|
||||||
|
accepts = cherrypy.request.headers.elements ('Accept')
|
||||||
|
for accept in accepts:
|
||||||
|
if accept.value in self.supported_book_mediatypes:
|
||||||
|
if accept.qvalue > 0:
|
||||||
|
client_accepted_book_mediatypes.append (accept.value)
|
||||||
|
|
||||||
|
return client_accepted_book_mediatypes
|
||||||
|
|
||||||
|
|
||||||
|
class NullPage (Page):
|
||||||
|
""" An empty page. """
|
||||||
|
|
||||||
|
def index (self, **dummy_kwargs):
|
||||||
|
""" Output an empty page. """
|
||||||
|
return '<html/>'
|
||||||
|
|
||||||
|
|
||||||
|
class SearchPage (Page):
|
||||||
|
""" Abstract base class for all search page classes. """
|
||||||
|
|
||||||
|
def setup (self, dummy_os, dummy_sql):
|
||||||
|
""" Let derived classes setup the query. """
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def fixup (self, os):
|
||||||
|
""" Give derived classes a chance to further manipulate database results. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finalize (self, os):
|
||||||
|
""" Give derived classes a chance to fix default finalization. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nothing_found (self, os):
|
||||||
|
""" Give derived class a chance to react if no records were found. """
|
||||||
|
os.entries.insert (0, self.no_records_found (os))
|
||||||
|
|
||||||
|
|
||||||
|
def output_suggestions (self, os, max_suggestions_per_word = 3):
|
||||||
|
""" Make suggestions. """
|
||||||
|
|
||||||
|
# similarity == matching_trigrams / (len1 + len2 - matching_trigrams)
|
||||||
|
|
||||||
|
sql_query = """
|
||||||
|
SELECT
|
||||||
|
word,
|
||||||
|
nentry,
|
||||||
|
similarity (word, %(word)s) AS sml
|
||||||
|
FROM terms
|
||||||
|
WHERE word %% %(word)s
|
||||||
|
ORDER BY sml DESC, nentry DESC LIMIT %(suggestions)s;"""
|
||||||
|
|
||||||
|
q = os.query.lower ()
|
||||||
|
sugg = []
|
||||||
|
for word in q.split ():
|
||||||
|
if len (word) > 3:
|
||||||
|
try:
|
||||||
|
rows = BaseSearcher.SQLSearcher().execute (
|
||||||
|
sql_query,
|
||||||
|
{ 'word': word, 'suggestions': max_suggestions_per_word + 1})
|
||||||
|
for i, row in enumerate (rows):
|
||||||
|
if i >= max_suggestions_per_word:
|
||||||
|
break
|
||||||
|
corr = row.word
|
||||||
|
if corr != word:
|
||||||
|
sugg.append ( (word, corr) )
|
||||||
|
except DatabaseError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for word, corr in reversed (sugg):
|
||||||
|
os.entries.insert (0, self.did_you_mean (os, corr, q.replace (word, corr)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output a search result page. """
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
os.log_request ('search')
|
||||||
|
|
||||||
|
if 'default_prefix' in kwargs:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Unknown parameter: default_prefix')
|
||||||
|
|
||||||
|
if os.start_index > BaseSearcher.MAX_RESULTS:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Parameter start_index too high')
|
||||||
|
|
||||||
|
sql = BaseSearcher.SQLStatement ()
|
||||||
|
sql.query = 'SELECT *'
|
||||||
|
sql.from_ = ['v_appserver_books_4 as books']
|
||||||
|
|
||||||
|
# let derived classes prepare the query
|
||||||
|
try:
|
||||||
|
self.setup (os, sql)
|
||||||
|
except ValueError as what:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. ' + str (what))
|
||||||
|
|
||||||
|
os.fix_sortorder ()
|
||||||
|
|
||||||
|
# execute the query
|
||||||
|
try:
|
||||||
|
BaseSearcher.SQLSearcher ().search (os, sql)
|
||||||
|
except DatabaseError as what:
|
||||||
|
cherrypy.log ("SQL Error: " + str (what),
|
||||||
|
context = 'REQUEST', severity = logging.ERROR)
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Check your query.')
|
||||||
|
|
||||||
|
# sync os.title and first entry header
|
||||||
|
if os.entries:
|
||||||
|
entry = os.entries[0]
|
||||||
|
if os.title and not entry.header:
|
||||||
|
entry.header = os.title
|
||||||
|
elif entry.header and not os.title:
|
||||||
|
os.title = entry.header
|
||||||
|
|
||||||
|
os.template = os.page = 'results'
|
||||||
|
|
||||||
|
# give derived class a chance to tweak result set
|
||||||
|
self.fixup (os)
|
||||||
|
|
||||||
|
# warn user about no records found
|
||||||
|
if os.total_results == 0:
|
||||||
|
self.nothing_found (os)
|
||||||
|
|
||||||
|
# suggest alternate queries
|
||||||
|
if os.total_results < 5:
|
||||||
|
self.output_suggestions (os)
|
||||||
|
|
||||||
|
# add sort by links
|
||||||
|
if os.start_index == 1 and os.total_results > 1:
|
||||||
|
if 'downloads' in os.alternate_sort_orders:
|
||||||
|
self.sort_by_downloads (os)
|
||||||
|
if 'release_date' in os.alternate_sort_orders:
|
||||||
|
self.sort_by_release_date (os)
|
||||||
|
if 'title' in os.alternate_sort_orders:
|
||||||
|
self.sort_by_title (os)
|
||||||
|
if 'alpha' in os.alternate_sort_orders:
|
||||||
|
self.sort_alphabetically (os)
|
||||||
|
if 'quantity' in os.alternate_sort_orders:
|
||||||
|
self.sort_by_quantity (os)
|
||||||
|
|
||||||
|
os.finalize ()
|
||||||
|
self.finalize (os)
|
||||||
|
|
||||||
|
if os.total_results > 0:
|
||||||
|
# call this after finalize ()
|
||||||
|
os.entries.insert (0, self.status_line (os))
|
||||||
|
|
||||||
|
return self.format (os)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_by_downloads (os):
|
||||||
|
""" Append the sort by downloads link. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'popular'
|
||||||
|
cat.title = _('Sort by Popularity')
|
||||||
|
cat.url = os.url_carry (sort_order = 'downloads')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'popular'
|
||||||
|
cat.order = 4.0
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_alphabetically (os):
|
||||||
|
""" Append the sort alphabetically link. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'alphabethical'
|
||||||
|
cat.title = _('Sort Alphabetically')
|
||||||
|
cat.url = os.url_carry (sort_order = 'alpha')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'alpha'
|
||||||
|
cat.order = 4.1
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_by_title (os):
|
||||||
|
""" Append the sort alphabetically link. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'alphabethical'
|
||||||
|
cat.title = _('Sort Alphabetically')
|
||||||
|
cat.url = os.url_carry (sort_order = 'title')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'alpha'
|
||||||
|
cat.order = 4.1
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_by_quantity (os):
|
||||||
|
""" Append the sort by quantity link. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'numerous'
|
||||||
|
cat.title = _('Sort by Quantity')
|
||||||
|
cat.url = os.url_carry (sort_order = 'quantity')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'quantity'
|
||||||
|
cat.order = 4.2
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort_by_release_date (os):
|
||||||
|
""" Append the sort by release date link. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'new'
|
||||||
|
cat.title = _('Sort by Release Date')
|
||||||
|
cat.url = os.url_carry (sort_order = 'release_date')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'date'
|
||||||
|
cat.order = 4.3
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def status_line (os):
|
||||||
|
""" Placeholder for status line. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = '__statusline__'
|
||||||
|
cat.class_ += 'grayed'
|
||||||
|
cat.icon = 'bibrec'
|
||||||
|
cat.order = 10
|
||||||
|
cat.header = os.title
|
||||||
|
cat.title = _(u"Displaying results {from_}–{to}").format (
|
||||||
|
from_ = os.start_index, to = os.end_index)
|
||||||
|
return cat
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def no_records_found (os):
|
||||||
|
""" Message. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = '__notfound__'
|
||||||
|
cat.title = _('No records found.')
|
||||||
|
cat.url = os.url ('start')
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'bibrec'
|
||||||
|
cat.order = 11
|
||||||
|
return cat
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def did_you_mean (os, corr, corrected_query):
|
||||||
|
""" Message. """
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = '__didyoumean__'
|
||||||
|
cat.title = _('Did you mean: {correction}').format (correction = corr)
|
||||||
|
cat.url = os.url ('search', query = corrected_query)
|
||||||
|
cat.class_ += 'navlink'
|
||||||
|
cat.icon = 'suggestion'
|
||||||
|
cat.order = 12
|
||||||
|
return cat
|
|
@ -0,0 +1,49 @@
|
||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
"repoze.lru" = "*"
|
||||||
|
Babel = "*"
|
||||||
|
chardet = "*"
|
||||||
|
CherryPy = "*"
|
||||||
|
colorama = "*"
|
||||||
|
#command-not-found = "*"
|
||||||
|
Genshi = "*"
|
||||||
|
html5lib = "*"
|
||||||
|
isodate = "*"
|
||||||
|
#language-selector = "*"
|
||||||
|
libgutenberg = "*"
|
||||||
|
netaddr = "*"
|
||||||
|
oauth = "*"
|
||||||
|
oauthlib = "*"
|
||||||
|
Pillow = "*"
|
||||||
|
psycopg2 = "*"
|
||||||
|
#pycurl = "*"
|
||||||
|
#PyGObject = "*"
|
||||||
|
pyparsing = "*"
|
||||||
|
#python-apt = "*"
|
||||||
|
pytz = "*"
|
||||||
|
qrcode = "*"
|
||||||
|
rdflib = "*"
|
||||||
|
recaptcha-client = "*"
|
||||||
|
regex = "*"
|
||||||
|
requests = "*"
|
||||||
|
requests-oauthlib = "*"
|
||||||
|
Routes = "*"
|
||||||
|
simplejson = "*"
|
||||||
|
six = "*"
|
||||||
|
SQLAlchemy = "*"
|
||||||
|
#ufw = "*"
|
||||||
|
#unattended-upgrades = "*"
|
||||||
|
urllib3 = "*"
|
||||||
|
adns = "==1.4-py0"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.6"
|
||||||
|
|
||||||
|
[pipenv]
|
||||||
|
allow_prereleases = true
|
|
@ -0,0 +1,413 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "ee8fb05228d77d9d0e14c78195d3ead551470bbfb9d44c0cbe88444aefe72370"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.6"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"adns": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1176e63b66db007e8798150a9a7cfcca5d8a810ae488905f8b2b6b07e678f2ef"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.4-py0"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||||
|
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"backports.functools-lru-cache": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a",
|
||||||
|
"sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd"
|
||||||
|
],
|
||||||
|
"version": "==1.5"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
|
||||||
|
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
|
||||||
|
],
|
||||||
|
"version": "==2019.3.9"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"cheroot": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:beb8eb9eeff5746059607e81b72efd6f4ca099111dc13f8961ae9e4f63f7786b",
|
||||||
|
"sha256:c52f8df52c461351b91ce9a7769208c064bae74bd61b531312ffc62bff667650"
|
||||||
|
],
|
||||||
|
"version": "==6.5.4"
|
||||||
|
},
|
||||||
|
"cherrypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4dd2f59b5af93bd9ca85f1ed0bb8295cd0f5a8ee2b84d476374d4e070aa5c615",
|
||||||
|
"sha256:626e305bca3c5d56a16e5f7d64bc8a4e25d26c41be1779f585fad2608edbc4c8"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==18.1.0"
|
||||||
|
},
|
||||||
|
"colorama": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||||
|
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.1"
|
||||||
|
},
|
||||||
|
"genshi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0d87ae62cf2ed92133f35725da51e02d09f79bb4cb986f0d948408a0279dd3f8",
|
||||||
|
"sha256:8dd0628da7898c625d487a676992490c8222e47bb56f36a3dcb0cfe28159997f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7.1"
|
||||||
|
},
|
||||||
|
"html5lib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
|
||||||
|
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||||
|
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.8"
|
||||||
|
},
|
||||||
|
"isodate": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
|
||||||
|
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.0"
|
||||||
|
},
|
||||||
|
"jaraco.functools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:35ba944f52b1a7beee8843a5aa6752d1d5b79893eeb7770ea98be6b637bf9345",
|
||||||
|
"sha256:e9e377644cee5f6f9128b4dab1631fca74981236e95a255f80e4292bcd2b5284"
|
||||||
|
],
|
||||||
|
"version": "==2.0"
|
||||||
|
},
|
||||||
|
"libgutenberg": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6afff1b3067f3a862aad28b981fb72d4ff0ee33a9bad0893d570c9cec3fcef20"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.1"
|
||||||
|
},
|
||||||
|
"lxml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0358b9e9642bc7d39aac5cffe9884a99a5ca68e5e2c1b89e570ed60da9139908",
|
||||||
|
"sha256:091a359c4dafebbecd3959d9013f1b896b5371859165e4e50b01607a98d9e3e2",
|
||||||
|
"sha256:1998e4e60603c64bcc35af61b4331ab3af087457900d3980e18d190e17c3a697",
|
||||||
|
"sha256:2000b4088dee9a41f459fddaf6609bba48a435ce6374bb254c5ccdaa8928c5ba",
|
||||||
|
"sha256:2afb0064780d8aaf165875be5898c1866766e56175714fa5f9d055433e92d41d",
|
||||||
|
"sha256:2d8f1d9334a4e3ff176d096c14ded3100547d73440683567d85b8842a53180bb",
|
||||||
|
"sha256:2e38db22f6a3199fd63675e1b4bd795d676d906869047398f29f38ca55cb453a",
|
||||||
|
"sha256:3181f84649c1a1ca62b19ddf28436b1b2cb05ae6c7d2628f33872e713994c364",
|
||||||
|
"sha256:37462170dfd88af8431d04de6b236e6e9c06cda71e2ca26d88ef2332fd2a5237",
|
||||||
|
"sha256:3a9d8521c89bf6f2a929c3d12ad3ad7392c774c327ea809fd08a13be6b3bc05f",
|
||||||
|
"sha256:3d0bbd2e1a28b4429f24fd63a122a450ce9edb7a8063d070790092d7343a1aa4",
|
||||||
|
"sha256:483d60585ce3ee71929cea70949059f83850fa5e12deb9c094ed1c8c2ec73cbd",
|
||||||
|
"sha256:4888be27d5cba55ce94209baef5bcd7bbd7314a3d17021a5fc10000b3a5f737d",
|
||||||
|
"sha256:64b0d62e4209170a2a0c404c446ab83b941a0003e96604d2e4f4cb735f8a2254",
|
||||||
|
"sha256:68010900898fdf139ac08549c4dba8206c584070a960ffc530aebf0c6f2794ef",
|
||||||
|
"sha256:872ecb066de602a0099db98bd9e57f4cfc1d62f6093d94460c787737aa08f39e",
|
||||||
|
"sha256:88a32b03f2e4cd0e63f154cac76724709f40b3fc2f30139eb5d6f900521b44ed",
|
||||||
|
"sha256:b1dc7683da4e67ab2bebf266afa68098d681ae02ce570f0d1117312273d2b2ac",
|
||||||
|
"sha256:b29e27ce9371810250cb1528a771d047a9c7b0f79630dc7dc5815ff828f4273b",
|
||||||
|
"sha256:ce197559596370d985f1ce6b7051b52126849d8159040293bf8b98cb2b3e1f78",
|
||||||
|
"sha256:d45cf6daaf22584eff2175f48f82c4aa24d8e72a44913c5aff801819bb73d11f",
|
||||||
|
"sha256:e2ff9496322b2ce947ba4a7a5eb048158de9d6f3fe9efce29f1e8dd6878561e6",
|
||||||
|
"sha256:f7b979518ec1f294a41a707c007d54d0f3b3e1fd15d5b26b7e99b62b10d9a72e",
|
||||||
|
"sha256:f9c7268e9d16e34e50f8246c4f24cf7353764affd2bc971f0379514c246e3f6b",
|
||||||
|
"sha256:f9c839806089d79de588ee1dde2dae05dc1156d3355dfeb2b51fde84d9c960ad",
|
||||||
|
"sha256:ff962953e2389226adc4d355e34a98b0b800984399153c6678f2367b11b4d4b8"
|
||||||
|
],
|
||||||
|
"version": "==4.3.2"
|
||||||
|
},
|
||||||
|
"more-itertools": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
|
||||||
|
"sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
|
||||||
|
],
|
||||||
|
"version": "==6.0.0"
|
||||||
|
},
|
||||||
|
"netaddr": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
|
||||||
|
"sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7.19"
|
||||||
|
},
|
||||||
|
"oauth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e769819ff0b0c043d020246ce1defcaadd65b9c21d244468a45a7f06cb88af5d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"oauthlib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298",
|
||||||
|
"sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.0.1"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e",
|
||||||
|
"sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7",
|
||||||
|
"sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a",
|
||||||
|
"sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3",
|
||||||
|
"sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1",
|
||||||
|
"sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1",
|
||||||
|
"sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7",
|
||||||
|
"sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1",
|
||||||
|
"sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3",
|
||||||
|
"sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055",
|
||||||
|
"sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf",
|
||||||
|
"sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f",
|
||||||
|
"sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f",
|
||||||
|
"sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239",
|
||||||
|
"sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe",
|
||||||
|
"sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c",
|
||||||
|
"sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697",
|
||||||
|
"sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494",
|
||||||
|
"sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356",
|
||||||
|
"sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6",
|
||||||
|
"sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000",
|
||||||
|
"sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f",
|
||||||
|
"sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c",
|
||||||
|
"sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca",
|
||||||
|
"sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8",
|
||||||
|
"sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3",
|
||||||
|
"sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad",
|
||||||
|
"sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9",
|
||||||
|
"sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc",
|
||||||
|
"sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.4.1"
|
||||||
|
},
|
||||||
|
"portend": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b7ce7d35ea262415297cbfea86226513e77b9ee5f631d3baa11992d663963719",
|
||||||
|
"sha256:f5c99a1aa1655733736bb0283fee6a1e115e18db500332bec8e24c43f320d8e8"
|
||||||
|
],
|
||||||
|
"version": "==2.3"
|
||||||
|
},
|
||||||
|
"psycopg2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02445ebbb3a11a3fe8202c413d5e6faf38bb75b4e336203ee144ca2c46529f94",
|
||||||
|
"sha256:0e9873e60f98f0c52339abf8f0339d1e22bfe5aae0bcf7aabd40c055175035ec",
|
||||||
|
"sha256:1148a5eb29073280bf9057c7fc45468592c1bb75a28f6df1591adb93c8cb63d0",
|
||||||
|
"sha256:259a8324e109d4922b0fcd046e223e289830e2568d6f4132a3702439e5fd532b",
|
||||||
|
"sha256:28dffa9ed4595429e61bacac41d3f9671bb613d1442ff43bcbec63d4f73ed5e8",
|
||||||
|
"sha256:314a74302d4737a3865d40ea50e430ce1543c921ba10f39d562e807cfe2edf2a",
|
||||||
|
"sha256:36b60201b6d215d7658a71493fdf6bd5e60ad9a0cffed39906627ff9f4f3afd3",
|
||||||
|
"sha256:3f9d532bce54c4234161176ff3b8688ff337575ca441ea27597e112dfcd0ee0c",
|
||||||
|
"sha256:5d222983847b40af989ad96c07fc3f07e47925e463baa5de716be8f805b41d9b",
|
||||||
|
"sha256:6757a6d2fc58f7d8f5d471ad180a0bd7b4dd3c7d681f051504fbea7ae29c8d6f",
|
||||||
|
"sha256:6a0e0f1e74edb0ab57d89680e59e7bfefad2bfbdf7c80eb38304d897d43674bb",
|
||||||
|
"sha256:6ca703ccdf734e886a1cf53eb702261110f6a8b0ed74bcad15f1399f74d3f189",
|
||||||
|
"sha256:8513b953d8f443c446aa79a4cc8a898bd415fc5e29349054f03a7d696d495542",
|
||||||
|
"sha256:9262a5ce2038570cb81b4d6413720484cb1bc52c064b2f36228d735b1f98b794",
|
||||||
|
"sha256:97441f851d862a0c844d981cbee7ee62566c322ebb3d68f86d66aa99d483985b",
|
||||||
|
"sha256:a07feade155eb8e69b54dd6774cf6acf2d936660c61d8123b8b6b1f9247b67d6",
|
||||||
|
"sha256:a9b9c02c91b1e3ec1f1886b2d0a90a0ea07cc529cb7e6e472b556bc20ce658f3",
|
||||||
|
"sha256:ae88216f94728d691b945983140bf40d51a1ff6c7fe57def93949bf9339ed54a",
|
||||||
|
"sha256:b360ffd17659491f1a6ad7c928350e229c7b7bd83a2b922b6ee541245c7a776f",
|
||||||
|
"sha256:b4221957ceccf14b2abdabef42d806e791350be10e21b260d7c9ce49012cc19e",
|
||||||
|
"sha256:b90758e49d5e6b152a460d10b92f8a6ccf318fcc0ee814dcf53f3a6fc5328789",
|
||||||
|
"sha256:c669ea986190ed05fb289d0c100cc88064351f2b85177cbfd3564c4f4847d18c",
|
||||||
|
"sha256:d1b61999d15c79cf7f4f7cc9021477aef35277fc52452cf50fd13b713c84424d",
|
||||||
|
"sha256:de7bb043d1adaaf46e38d47e7a5f703bb3dab01376111e522b07d25e1a79c1e1",
|
||||||
|
"sha256:e393568e288d884b94d263f2669215197840d097c7e5b0acd1a51c1ea7d1aba8",
|
||||||
|
"sha256:ed7e0849337bd37d89f2c2b0216a0de863399ee5d363d31b1e5330a99044737b",
|
||||||
|
"sha256:f153f71c3164665d269a5d03c7fa76ba675c7a8de9dc09a4e2c2cdc9936a7b41",
|
||||||
|
"sha256:f1fb5a8427af099beb7f65093cbdb52e021b8e6dbdfaf020402a623f4181baf5",
|
||||||
|
"sha256:f36b333e9f86a2fba960c72b90c34be6ca71819e300f7b1fc3d2b0f0b2c546cd",
|
||||||
|
"sha256:f4526d078aedd5187d0508aa5f9a01eae6a48a470ed678406da94b4cd6524b7e"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.7.7"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a",
|
||||||
|
"sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
|
||||||
|
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2018.9"
|
||||||
|
},
|
||||||
|
"qrcode": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5",
|
||||||
|
"sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==6.1"
|
||||||
|
},
|
||||||
|
"rdflib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:58d5994610105a457cff7fdfe3d683d87786c5028a45ae032982498a7e913d6f",
|
||||||
|
"sha256:da1df14552555c5c7715d8ce71c08f404c988c58a1ecd38552d0da4fc261280d"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.2.2"
|
||||||
|
},
|
||||||
|
"recaptcha-client": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:28c6853c1d13d365b7dc71a6b05e5ffb56471f70a850de318af50d3d7c0dea2f"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.6"
|
||||||
|
},
|
||||||
|
"regex": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0306149889c1a1bec362511f737bc446245ddfcdbe4b556abdfc506ed46dfa47",
|
||||||
|
"sha256:4b08704a5939c698d2d5950b5dc950597613216cc8c01048efc0da860a0c3db9",
|
||||||
|
"sha256:6ba0eb777ada6887062c2620e6d644b011078d5d3dc09119ae7107285f6f95e9",
|
||||||
|
"sha256:7789cc323948792c4c62b269a56f2f2f9bc77d44e54fd81e01b12a967dd7244c",
|
||||||
|
"sha256:825143aadca0da7d26eeaf2ab0f8bc33921a5642e570ded92dde08c5aaebc65f",
|
||||||
|
"sha256:8fbd057faab28ce552d89c46f7a968e950f07e80752dfb93891dd11c6b0ee3b4",
|
||||||
|
"sha256:a41aabb0b9072a14f1e2e554f959ed6439b83610ed656edace9096a0b27e378e",
|
||||||
|
"sha256:d8807231aed332a1d0456d2088967b87e8c664222bed8e566384ca0ec0b43bfd",
|
||||||
|
"sha256:dfd89b642fe71f4e8a9906455d4147d453061377b650e6233ddd9ea822971360"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2019.3.12"
|
||||||
|
},
|
||||||
|
"repoze.lru": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0429a75e19380e4ed50c0694e26ac8819b4ea7851ee1fc7583c8572db80aff77",
|
||||||
|
"sha256:f77bf0e1096ea445beadd35f3479c5cff2aa1efe604a133e67150bc8630a62ea"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
|
||||||
|
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.21.0"
|
||||||
|
},
|
||||||
|
"requests-oauthlib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57",
|
||||||
|
"sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"routes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:26ee43340fca5a32769ffe0c58edcb396ccce6bc1dfa689ddf844d50877355fd",
|
||||||
|
"sha256:d64b8ae22bef127d856afd9266a3e4cfc9e0dda0e120195e38268a95d20de135"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.1"
|
||||||
|
},
|
||||||
|
"simplejson": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:067a7177ddfa32e1483ba5169ebea1bc2ea27f224853211ca669325648ca5642",
|
||||||
|
"sha256:2fc546e6af49fb45b93bbe878dea4c48edc34083729c0abd09981fe55bdf7f91",
|
||||||
|
"sha256:354fa32b02885e6dae925f1b5bbf842c333c1e11ea5453ddd67309dc31fdb40a",
|
||||||
|
"sha256:37e685986cf6f8144607f90340cff72d36acf654f3653a6c47b84c5c38d00df7",
|
||||||
|
"sha256:3af610ee72efbe644e19d5eaad575c73fb83026192114e5f6719f4901097fce2",
|
||||||
|
"sha256:3b919fc9cf508f13b929a9b274c40786036b31ad28657819b3b9ba44ba651f50",
|
||||||
|
"sha256:3dd289368bbd064974d9a5961101f080e939cbe051e6689a193c99fb6e9ac89b",
|
||||||
|
"sha256:6c3258ffff58712818a233b9737fe4be943d306c40cf63d14ddc82ba563f483a",
|
||||||
|
"sha256:75e3f0b12c28945c08f54350d91e624f8dd580ab74fd4f1bbea54bc6b0165610",
|
||||||
|
"sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5",
|
||||||
|
"sha256:ee9625fc8ee164902dfbb0ff932b26df112da9f871c32f0f9c1bcf20c350fe2a",
|
||||||
|
"sha256:fb2530b53c28f0d4d84990e945c2ebb470edb469d63e389bf02ff409012fe7c5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.16.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||||
|
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.12.0"
|
||||||
|
},
|
||||||
|
"sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.1"
|
||||||
|
},
|
||||||
|
"tempora": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4951da790bd369f718dbe2287adbdc289dc2575a09278e77fad6131bcfe93097",
|
||||||
|
"sha256:f8abbbd486eca3340bd3d242417b203c861d4e113ef778cd5fb9535b2b32ae54"
|
||||||
|
],
|
||||||
|
"version": "==1.14"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||||
|
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.24.1"
|
||||||
|
},
|
||||||
|
"webencodings": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||||
|
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||||
|
],
|
||||||
|
"version": "==0.5.1"
|
||||||
|
},
|
||||||
|
"zc.lockfile": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:95a8e3846937ab2991b61703d6e0251d5abb9604e18412e2714e1b90db173253"
|
||||||
|
],
|
||||||
|
"version": "==1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
PostgresSession.py
|
||||||
|
|
||||||
|
Copyright 2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
A rewrite of the cherrypy PostgresqlSession.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
import PostgresSession
|
||||||
|
cherrypy.lib.sessions.PostgresSession = PostgresSession.PostgresSession
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import cherrypy.lib.sessions
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresSession (cherrypy.lib.sessions.Session):
|
||||||
|
""" Implementation of the PostgreSQL backend for sessions. It assumes
|
||||||
|
a table like this::
|
||||||
|
|
||||||
|
create table <table_name> (
|
||||||
|
id varchar (40) primary key,
|
||||||
|
expires timestamp,
|
||||||
|
data bytea
|
||||||
|
)
|
||||||
|
|
||||||
|
You must provide your own `get_dbapi20_connection ()` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||||
|
select = 'select expires, data from table_name where id=%(id)s for update'
|
||||||
|
|
||||||
|
|
||||||
|
def __init__ (self, id_ = None, **kwargs):
|
||||||
|
self.table_name = kwargs.get ('table', 'session')
|
||||||
|
# Session.__init__ () may need working connection
|
||||||
|
self.connection = self.get_dbapi20_connection ()
|
||||||
|
|
||||||
|
super (PostgresSession, self).__init__ (id_, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dbapi20_connection ():
|
||||||
|
""" Return a dbapi 2.0 compatible connection. """
|
||||||
|
return cherrypy.engine.pool.connect ()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup (cls, **kwargs):
|
||||||
|
"""Set up the storage system for Postgres-based sessions.
|
||||||
|
|
||||||
|
This should only be called once per process; this will be done
|
||||||
|
automatically when using sessions.init (as the built-in Tool does).
|
||||||
|
"""
|
||||||
|
|
||||||
|
cherrypy.log ("Using PostgresSession",
|
||||||
|
context = 'SESSION', severity = logging.INFO)
|
||||||
|
|
||||||
|
for k, v in kwargs.items ():
|
||||||
|
setattr (cls, k, v)
|
||||||
|
|
||||||
|
|
||||||
|
def now (self):
|
||||||
|
"""Generate the session specific concept of 'now'.
|
||||||
|
|
||||||
|
Other session providers can override this to use alternative,
|
||||||
|
possibly timezone aware, versions of 'now'.
|
||||||
|
"""
|
||||||
|
return datetime.datetime.utcnow ()
|
||||||
|
|
||||||
|
|
||||||
|
def _exec (self, sql, **kwargs):
|
||||||
|
""" Internal helper to execute sql statements. """
|
||||||
|
|
||||||
|
kwargs['id'] = self.id
|
||||||
|
cursor = self.connection.cursor ()
|
||||||
|
cursor.execute (sql.replace ('table_name', self.table_name), kwargs)
|
||||||
|
return cursor
|
||||||
|
|
||||||
|
|
||||||
|
def _exists (self):
|
||||||
|
""" Return true if session data exists. """
|
||||||
|
cursor = self._exec (self.select)
|
||||||
|
return bool (cursor.fetchall ())
|
||||||
|
|
||||||
|
|
||||||
|
def _load (self):
|
||||||
|
""" Load the session data. """
|
||||||
|
|
||||||
|
cursor = self._exec (self.select)
|
||||||
|
rows = cursor.fetchall ()
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
expires, pickled_data = rows[0]
|
||||||
|
data = pickle.loads (pickled_data)
|
||||||
|
return data, expires
|
||||||
|
|
||||||
|
|
||||||
|
def _save (self, expires):
|
||||||
|
""" Save the session data. """
|
||||||
|
|
||||||
|
pickled_data = pickle.dumps (self._data, self.pickle_protocol)
|
||||||
|
|
||||||
|
self._delete ()
|
||||||
|
self._exec (
|
||||||
|
"""\
|
||||||
|
insert into table_name (id, expires, data)
|
||||||
|
values (%(id)s, %(expires)s, %(data)s)
|
||||||
|
""",
|
||||||
|
data = pickled_data,
|
||||||
|
expires = expires
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete (self):
|
||||||
|
""" Delete the session data. """
|
||||||
|
self._exec ('delete from table_name where id=%(id)s')
|
||||||
|
|
||||||
|
|
||||||
|
def acquire_lock (self):
|
||||||
|
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||||
|
|
||||||
|
self._exec (self.select)
|
||||||
|
self.locked = True
|
||||||
|
|
||||||
|
|
||||||
|
def release_lock (self):
|
||||||
|
"""Release the lock on the currently-loaded session data."""
|
||||||
|
|
||||||
|
self.connection.commit ()
|
||||||
|
self.locked = False
|
||||||
|
|
||||||
|
|
||||||
|
def clean_up (self):
|
||||||
|
"""Clean up expired sessions."""
|
||||||
|
|
||||||
|
self._exec (
|
||||||
|
'delete from table_name where expires < %(now)s',
|
||||||
|
now = self.now ()
|
||||||
|
)
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
QRCodePage.py
|
||||||
|
|
||||||
|
Copyright 2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
A page to generate QR-codes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import six
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import qrcode
|
||||||
|
|
||||||
|
|
||||||
|
class QRCodePage (object):
|
||||||
|
""" Serve a QR-code as PNG image. """
|
||||||
|
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output QR-Code.
|
||||||
|
|
||||||
|
Parameters are:
|
||||||
|
|
||||||
|
data: the data to encode (url quoted)
|
||||||
|
ec_level: error correction level. One of: L M Q H
|
||||||
|
version: QR code version
|
||||||
|
box_size: size of one QR code box in pixel
|
||||||
|
border: width of border in boxes (should be at least 4)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
qr = qrcode.QRCode (
|
||||||
|
error_correction = self._get_ecl (kwargs),
|
||||||
|
version = kwargs.get ('version', None),
|
||||||
|
box_size = kwargs.get ('box_size', 10),
|
||||||
|
border = kwargs.get ('border', 4),
|
||||||
|
)
|
||||||
|
|
||||||
|
qr.add_data (urllib.parse.unquote (kwargs['data']))
|
||||||
|
qr.make (fit = True)
|
||||||
|
|
||||||
|
img = qr.make_image ()
|
||||||
|
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'image/png'
|
||||||
|
|
||||||
|
buf = six.BytesIO ()
|
||||||
|
img._img.save (buf, 'PNG')
|
||||||
|
return buf.getvalue ()
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_ecl (kwargs):
|
||||||
|
""" Get and decode error correction paramter. """
|
||||||
|
|
||||||
|
ecl = {
|
||||||
|
'L': 1,
|
||||||
|
'M': 0,
|
||||||
|
'Q': 3,
|
||||||
|
'H': 2,
|
||||||
|
}
|
||||||
|
if 'ec_level' in kwargs and kwargs['ec_level'] in ecl:
|
||||||
|
return ecl[kwargs['ec_level']]
|
||||||
|
return ecl['M']
|
|
@ -0,0 +1,32 @@
|
||||||
|
# autocat3_original
|
||||||
|
## Intruduction
|
||||||
|
|
||||||
|
**autocat3** is a python-based application used for supporting [Project Gutenberg](gutenberg.org).
|
||||||
|
CherryPy is used as the web framwork which is easy to develop.
|
||||||
|
If mainly implemented the search functionality and rate limiter. Also return results pages based on templates.
|
||||||
|
|
||||||
|
## How it works.
|
||||||
|
Currently, the dev environment and production environment share the same server **login1**.
|
||||||
|
The production version of autocat3 is on **gutenberg2**.
|
||||||
|
This application in this repository is on **gutenberg1**.
|
||||||
|
|
||||||
|
Previously, the old version of autocat3 relies on dependencies installed directly on the system. To make it more flexible and easy to deploy, we tend to use virtual env rather than the previous method. To use virtual env, we use pipenv instead of using pip and virtual env separately.
|
||||||
|
|
||||||
|
The virtual env directory is on the default directory while we run ```pipenv --three```. So it's not in this directory. (we are only using python3 for this project because CherryPy will discard the python2 in the future).
|
||||||
|
|
||||||
|
To start the service/application, we use **systemd** to do that. the ```autocat3.service``` file is written under ```/etc/systemd/system```directory.
|
||||||
|
*To start*:
|
||||||
|
1. make sure ```sudo systemctl daemon-reload``` everytime we edit the systemd unit file
|
||||||
|
2. ```sudo systemctl start autocat3.service``` to start service
|
||||||
|
3. ```sudo systemctl stop autocat3.service``` to stop service
|
||||||
|
4. ```sudo systemctl status autocat3.service``` to check the running status of the service
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
Currently, we use the following steps to deploy autocat3 on a different server.
|
||||||
|
1. **Create Virtual env**: ```pipenv --three``` to create a virtual env for current working directory(current project)
|
||||||
|
2. **Install packages/python modules**: ```pipenv install``` to install all the packages in the Pipfile. If there is a requirements.txt file output from ```pip freeze```, the command will automatically add the package names into Pipfile and install the packages and keep them in the Popfile for later use.
|
||||||
|
3. **lock the packages**: ```pipenv lock``` to be used to produce deterministic builds.
|
||||||
|
4. **Check the virtual env path**: ```pipenv --venv```
|
||||||
|
5. **Start virtual env**: ```pipenv shell```
|
||||||
|
|
||||||
|
Copyright 2009-2010 by Marcello Perathoner
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,365 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SearchPage.py
|
||||||
|
|
||||||
|
Copyright 2009-2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The various flavors of search page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
from libgutenberg.DublinCore import DublinCore
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
from Page import SearchPage
|
||||||
|
|
||||||
|
|
||||||
|
class BookSearchPage (SearchPage):
|
||||||
|
""" search term => list of books """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.sort_orders = ('downloads', 'release_date', 'title', 'random')
|
||||||
|
os.icon = 'book'
|
||||||
|
os.class_ += 'booklink'
|
||||||
|
os.f_format_icon = os.format_icon_titles
|
||||||
|
|
||||||
|
if os.sort_order == 'random':
|
||||||
|
sql.where.append ("""
|
||||||
|
pk in (select floor (random () * maxbook)::integer
|
||||||
|
from generate_series (1, 30), (select max (pk) as maxbook
|
||||||
|
from books) xbks1)
|
||||||
|
""")
|
||||||
|
|
||||||
|
if len (os.query):
|
||||||
|
sql.fulltext ('books.tsvec', os.query)
|
||||||
|
os.title = _("Books: {title}").format (title = os.query)
|
||||||
|
else:
|
||||||
|
os.title = _('All Books')
|
||||||
|
|
||||||
|
|
||||||
|
def fixup (self, os):
|
||||||
|
""" strip marc subfields, add social media hints and facet links """
|
||||||
|
|
||||||
|
for e in os.entries:
|
||||||
|
if '$' in e.title:
|
||||||
|
e.title = DublinCore.strip_marc_subfields (e.title)
|
||||||
|
|
||||||
|
if (os.sort_order == 'release_date' and os.total_results > 0 and os.start_index == 1):
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('Follow new books on Twitter')
|
||||||
|
cat.subtitle = _("Follow our new books on Twitter.")
|
||||||
|
cat.url = 'https://twitter.com/gutenberg_new'
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'twitter'
|
||||||
|
cat.order = 5
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('Follow new books on Facebook')
|
||||||
|
cat.subtitle = _("Follow the link and like the page to have us post new books to your wall.")
|
||||||
|
cat.url = 'https://www.facebook.com/gutenberg.new'
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'facebook'
|
||||||
|
cat.order = 5
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
if (len (os.query) and os.start_index == 1):
|
||||||
|
sql2 = BaseSearcher.SQLStatement ()
|
||||||
|
sql2.query = "select count (*) from bookshelves"
|
||||||
|
sql2.fulltext ('bookshelves.tsvec', os.query)
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (*sql2.build ())
|
||||||
|
if rows[0][0] > 0:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.title = _('Bookshelves')
|
||||||
|
cat.subtitle = __('One bookshelf matches your query.',
|
||||||
|
'{count} bookshelves match your search.',
|
||||||
|
rows[0][0]).format (count = rows[0][0])
|
||||||
|
cat.url = os.url ('bookshelf_search', query = os.query)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'bookshelf'
|
||||||
|
cat.order = 3
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
sql2 = BaseSearcher.SQLStatement ()
|
||||||
|
sql2.query = "select count (*) from subjects"
|
||||||
|
sql2.fulltext ('subjects.tsvec', os.query)
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (*sql2.build ())
|
||||||
|
if rows[0][0] > 0:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.title = _('Subjects')
|
||||||
|
cat.subtitle = __('One subject heading matches your search.',
|
||||||
|
'{count} subject headings match your search.',
|
||||||
|
rows[0][0]).format (count = rows[0][0])
|
||||||
|
cat.url = os.url ('subject_search', query = os.query)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'subject'
|
||||||
|
cat.order = 3
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
sql2 = BaseSearcher.SQLStatement ()
|
||||||
|
sql2.query = "select count (*) from authors"
|
||||||
|
sql2.fulltext ('authors.tsvec', os.query)
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (*sql2.build ())
|
||||||
|
if rows[0][0] > 0:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.title = _('Authors')
|
||||||
|
cat.subtitle = __('One author name matches your search.',
|
||||||
|
'{count} author names match your search.',
|
||||||
|
rows[0][0]).format (count = rows[0][0])
|
||||||
|
cat.url = os.url ('author_search', query = os.query)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'author'
|
||||||
|
cat.order = 3
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorSearchPage (SearchPage):
|
||||||
|
""" name => list of authors """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.f_format_subtitle = os.format_subtitle
|
||||||
|
os.f_format_url = BaseSearcher.SearchUrlFormatter ('author')
|
||||||
|
os.f_format_thumb_url = os.format_none
|
||||||
|
os.sort_orders = ('downloads', 'quantity', 'alpha', 'release_date')
|
||||||
|
os.icon = 'author'
|
||||||
|
os.class_ += 'navlink'
|
||||||
|
os.title = _('All Authors')
|
||||||
|
|
||||||
|
sql.query = """
|
||||||
|
SELECT
|
||||||
|
authors.author as title,
|
||||||
|
coalesce (authors.born_floor || '', '') || '-' ||
|
||||||
|
coalesce (authors.died_floor || '', '') as subtitle,
|
||||||
|
authors.pk as pk,
|
||||||
|
max (books.release_date) as release_date,
|
||||||
|
sum (books.downloads) as downloads,
|
||||||
|
count (books.pk) as quantity"""
|
||||||
|
|
||||||
|
sql.from_ = ('authors', 'mn_books_authors as mn', 'books')
|
||||||
|
sql.groupby += ('authors.author', 'subtitle', 'authors.pk')
|
||||||
|
sql.where.append ('authors.pk = mn.fk_authors')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
|
||||||
|
if len (os.query):
|
||||||
|
sql.fulltext ('authors.tsvec', os.query)
|
||||||
|
os.title = _("Authors: {author}").format (author = os.query)
|
||||||
|
else:
|
||||||
|
sql.where.append ("authors.author not in ('Various', 'Anonymous', 'Unknown')")
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectSearchPage (SearchPage):
|
||||||
|
""" term => list of subects """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.f_format_url = BaseSearcher.SearchUrlFormatter ('subject')
|
||||||
|
os.f_format_thumb_url = os.format_none
|
||||||
|
os.sort_orders = ('downloads', 'quantity', 'alpha', 'release_date')
|
||||||
|
os.icon = 'subject'
|
||||||
|
os.class_ += 'navlink'
|
||||||
|
os.title = _('All Subjects')
|
||||||
|
|
||||||
|
sql.query = """
|
||||||
|
SELECT
|
||||||
|
subjects.subject as title,
|
||||||
|
subjects.pk as pk,
|
||||||
|
max (books.release_date) as release_date,
|
||||||
|
sum (books.downloads) as downloads,
|
||||||
|
count (books.pk) as quantity"""
|
||||||
|
|
||||||
|
sql.from_ = ('subjects', 'mn_books_subjects as mn', 'books')
|
||||||
|
sql.groupby += ('subjects.subject', 'subjects.pk')
|
||||||
|
sql.where.append ('subjects.pk = mn.fk_subjects')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
|
||||||
|
if len (os.query):
|
||||||
|
sql.fulltext ('subjects.tsvec', os.query)
|
||||||
|
os.title = _("Subjects: {subject}").format (subject = os.query)
|
||||||
|
|
||||||
|
|
||||||
|
class BookshelfSearchPage (SearchPage):
|
||||||
|
""" term => list of bookshelves """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.f_format_url = BaseSearcher.SearchUrlFormatter ('bookshelf')
|
||||||
|
os.f_format_thumb_url = os.format_none
|
||||||
|
os.sort_orders = ('downloads', 'quantity', 'alpha', 'release_date')
|
||||||
|
os.icon = 'bookshelf'
|
||||||
|
os.class_ += 'navlink'
|
||||||
|
os.title = _('All Bookshelves')
|
||||||
|
|
||||||
|
sql.query = """
|
||||||
|
SELECT
|
||||||
|
bookshelves.bookshelf as title,
|
||||||
|
bookshelves.pk as pk,
|
||||||
|
max (books.release_date) as release_date,
|
||||||
|
sum (books.downloads) as downloads,
|
||||||
|
count (books.pk) as quantity"""
|
||||||
|
|
||||||
|
sql.from_ = ('bookshelves', 'mn_books_bookshelves as mn', 'books')
|
||||||
|
sql.groupby += ('bookshelves.bookshelf', 'bookshelves.pk')
|
||||||
|
sql.where.append ('bookshelves.pk = mn.fk_bookshelves')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
|
||||||
|
if len (os.query):
|
||||||
|
sql.fulltext ('bookshelves.tsvec', os.query)
|
||||||
|
os.title = _("Bookshelves: {bookshelf}").format (bookshelf = os.query)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorPage (SearchPage):
|
||||||
|
""" author id => books by author """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.sort_orders = ('downloads', 'title', 'release_date')
|
||||||
|
os.title_icon = 'author'
|
||||||
|
os.icon = 'book'
|
||||||
|
os.class_ += 'booklink'
|
||||||
|
os.f_format_icon = os.format_icon_titles
|
||||||
|
os.author = BaseSearcher.sql_get (
|
||||||
|
"select author from authors where pk = %(pk)s", pk = os.id)
|
||||||
|
os.title = _('Books by {author}').format (author = os.author)
|
||||||
|
|
||||||
|
sql.from_.append ('mn_books_authors as mn')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
sql.where.append ("mn.fk_authors = %(fk_authors)s")
|
||||||
|
sql.params['fk_authors'] = os.id
|
||||||
|
|
||||||
|
def fixup (self, os):
|
||||||
|
|
||||||
|
if (os.start_index == 1 and len (os.entries) > 1):
|
||||||
|
|
||||||
|
# browse-by-author page for maintainers
|
||||||
|
if 'is-catalog-maintainer' in cherrypy.request.cookie:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.type = mt.html
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.title = _('Browse by Author')
|
||||||
|
cat.url = "/browse/authors/%s#a%d" % (os.author[:1].lower (), os.id)
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'internal'
|
||||||
|
cat.order = 9
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
# wikipedia links etc.
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (
|
||||||
|
"""SELECT url, description AS title FROM author_urls
|
||||||
|
WHERE fk_authors = %(fk_authors)s""",
|
||||||
|
{ 'fk_authors': os.id } )
|
||||||
|
for row in rows:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.type = mt.html
|
||||||
|
cat.rel = 'related'
|
||||||
|
cat.title = _('See also: {title}').format (title = row.title)
|
||||||
|
cat.url = row.url
|
||||||
|
cat.class_ += 'navlink grayed'
|
||||||
|
cat.icon = 'external'
|
||||||
|
cat.order = 8
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
# author aliases
|
||||||
|
if os.format in ('html', 'mobile'):
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (
|
||||||
|
"""SELECT alias AS title FROM aliases
|
||||||
|
WHERE fk_authors = %(fk_authors)s AND alias_heading = 1""",
|
||||||
|
{ 'fk_authors': os.id }
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('Alias {alias}').format (alias = row.title)
|
||||||
|
cat.class_ += 'grayed'
|
||||||
|
cat.icon = 'alias'
|
||||||
|
cat.order = 7
|
||||||
|
os.entries.insert (0, cat)
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectPage (SearchPage):
|
||||||
|
""" subject id => books about subject """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.sort_orders = ('downloads', 'title', 'release_date')
|
||||||
|
os.title_icon = 'subject'
|
||||||
|
os.icon = 'book'
|
||||||
|
os.class_ += 'booklink'
|
||||||
|
os.f_format_icon = os.format_icon_titles
|
||||||
|
os.subject = BaseSearcher.sql_get (
|
||||||
|
"select subject from subjects where pk = %(pk)s", pk = os.id)
|
||||||
|
os.title = _('Books about {subject}').format (subject = os.subject)
|
||||||
|
|
||||||
|
sql.from_.append ('mn_books_subjects as mn')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
sql.where.append ("mn.fk_subjects = %(fk_subjects)s")
|
||||||
|
sql.params['fk_subjects'] = os.id
|
||||||
|
|
||||||
|
|
||||||
|
class BookshelfPage (SearchPage):
|
||||||
|
""" bookshelf id => books on bookshelf """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.sort_orders = ('downloads', 'title', 'release_date')
|
||||||
|
os.title_icon = 'bookshelf'
|
||||||
|
os.icon = 'book'
|
||||||
|
os.class_ += 'booklink'
|
||||||
|
os.f_format_icon = os.format_icon_titles
|
||||||
|
os.bookshelf = BaseSearcher.sql_get (
|
||||||
|
"select bookshelf from bookshelves where pk = %(pk)s", pk = os.id)
|
||||||
|
os.title = _('Books in {bookshelf}').format (bookshelf = os.bookshelf)
|
||||||
|
|
||||||
|
sql.from_.append ('mn_books_bookshelves as mn')
|
||||||
|
sql.where.append ('books.pk = mn.fk_books')
|
||||||
|
sql.where.append ("mn.fk_bookshelves = %(fk_bookshelves)s")
|
||||||
|
sql.params['fk_bookshelves'] = os.id
|
||||||
|
|
||||||
|
|
||||||
|
class AlsoDownloadedPage (SearchPage):
|
||||||
|
""" ebook id => books people also downloaded """
|
||||||
|
|
||||||
|
def setup (self, os, sql):
|
||||||
|
os.sort_orders = ('downloads', )
|
||||||
|
os.icon = 'book'
|
||||||
|
os.class_ += 'booklink'
|
||||||
|
os.f_format_icon = os.format_icon_titles
|
||||||
|
os.title = _('Readers also downloaded')
|
||||||
|
|
||||||
|
sql.query = """
|
||||||
|
SELECT
|
||||||
|
books.pk,
|
||||||
|
books.title,
|
||||||
|
books.filing,
|
||||||
|
books.author,
|
||||||
|
books.release_date,
|
||||||
|
books.fk_categories,
|
||||||
|
books.fk_langs,
|
||||||
|
books.coverpages,
|
||||||
|
d.dl as downloads
|
||||||
|
FROM
|
||||||
|
v_appserver_books_4 as books
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
s1.fk_books as pk, count (s1.id) as dl
|
||||||
|
FROM
|
||||||
|
scores.also_downloads as s1,
|
||||||
|
scores.also_downloads as s2
|
||||||
|
WHERE s2.fk_books = %(fk_books)s
|
||||||
|
AND s1.fk_books != %(fk_books)s
|
||||||
|
AND s1.id = s2.id
|
||||||
|
GROUP BY s1.fk_books) as d
|
||||||
|
ON d.pk = books.pk"""
|
||||||
|
sql.from_ = ()
|
||||||
|
sql.params['fk_books'] = os.id
|
||||||
|
|
||||||
|
|
||||||
|
def finalize (self, os):
|
||||||
|
# one page is enough
|
||||||
|
os.show_next_page_link = False
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sitemap.py
|
||||||
|
|
||||||
|
Copyright 2009-2013 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Output a Google sitemap.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg.GutenbergGlobals import Struct
|
||||||
|
|
||||||
|
import TemplatedPage
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
SITEMAP_SIZE = 1000 # max no. of urls to put into one sitemap
|
||||||
|
|
||||||
|
|
||||||
|
class Sitemap (TemplatedPage.TemplatedPage):
|
||||||
|
""" Output Google sitemap. """
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output sitemap. """
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
start = int (kwargs['page']) * SITEMAP_SIZE
|
||||||
|
|
||||||
|
rows = BaseSearcher.SQLSearcher.execute (
|
||||||
|
'select pk from books where pk >= %(start)s and pk < %(end)s order by pk',
|
||||||
|
{ 'start': str (start), 'end': str (start + SITEMAP_SIZE) })
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
host = cherrypy.config['host']
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
url = Struct ()
|
||||||
|
url.loc = os.url ('bibrec', id = row[0], host = host, format = None)
|
||||||
|
urls.append (url)
|
||||||
|
|
||||||
|
data = Struct ()
|
||||||
|
data.urls = urls
|
||||||
|
|
||||||
|
return self.output ('sitemap', data = data)
|
||||||
|
|
||||||
|
|
||||||
|
class SitemapIndex (TemplatedPage.TemplatedPage):
|
||||||
|
""" Output Google sitemap index. """
|
||||||
|
|
||||||
|
def index (self, **dummy_kwargs):
|
||||||
|
""" Output sitemap index. """
|
||||||
|
|
||||||
|
sitemaps = []
|
||||||
|
now = datetime.datetime.utcnow ().replace (microsecond = 0).isoformat () + 'Z'
|
||||||
|
|
||||||
|
# 99999 is safeguard against bogus ebook numbers
|
||||||
|
lastbook = BaseSearcher.sql_get ('select max (pk) as lastbook from books where pk < 99999')
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
host = cherrypy.config['host']
|
||||||
|
|
||||||
|
for n in range (0, lastbook // SITEMAP_SIZE + 1):
|
||||||
|
sitemap = Struct ()
|
||||||
|
sitemap.loc = os.url ('sitemap_index', page = n, host = host, format = None)
|
||||||
|
sitemap.lastmod = now
|
||||||
|
sitemaps.append (sitemap)
|
||||||
|
|
||||||
|
data = Struct ()
|
||||||
|
data.sitemaps = sitemaps
|
||||||
|
|
||||||
|
return self.output ('sitemap-index', data = data)
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
StartPage.py
|
||||||
|
|
||||||
|
Copyright 2009-2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The Search Start Page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import Page
|
||||||
|
|
||||||
|
class Start (Page.Page):
|
||||||
|
""" The start page. """
|
||||||
|
|
||||||
|
def index (self, **dummy_kwargs):
|
||||||
|
""" Output the start page. """
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
|
||||||
|
os.log_request ('start')
|
||||||
|
|
||||||
|
os.search_terms = ''
|
||||||
|
os.title = {
|
||||||
|
'mobile': _('PG Mobile'),
|
||||||
|
'opds': _('Project Gutenberg'),
|
||||||
|
'stanza': _('Project Gutenberg')
|
||||||
|
}.get (os.format, _('Search Project Gutenberg'))
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.header = _(
|
||||||
|
'Welcome to Project Gutenberg. Use the search box to find your book or pick a link.')
|
||||||
|
cat.title = _('Popular')
|
||||||
|
cat.subtitle = _('Our most popular books.')
|
||||||
|
cat.url = os.url ('search', sort_order = 'downloads')
|
||||||
|
cat.class_ += 'navlink'
|
||||||
|
cat.icon = 'popular'
|
||||||
|
cat.order = 2
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('Latest')
|
||||||
|
cat.subtitle = _('Our latest releases.')
|
||||||
|
cat.url = os.url ('search', sort_order = 'release_date')
|
||||||
|
cat.class_ += 'navlink'
|
||||||
|
cat.icon = 'date'
|
||||||
|
cat.order = 3
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
cat = BaseSearcher.Cat ()
|
||||||
|
cat.title = _('Random')
|
||||||
|
cat.subtitle = _('Random books.')
|
||||||
|
cat.url = os.url ('search', sort_order = 'random')
|
||||||
|
cat.class_ += 'navlink'
|
||||||
|
cat.icon = 'random'
|
||||||
|
cat.order = 4
|
||||||
|
os.entries.append (cat)
|
||||||
|
|
||||||
|
os.total_results = 0
|
||||||
|
os.template = 'results'
|
||||||
|
os.page = 'start'
|
||||||
|
|
||||||
|
os.url_share = os.url ('/', host = os.file_host)
|
||||||
|
os.twit = os.tagline
|
||||||
|
|
||||||
|
os.finalize ()
|
||||||
|
|
||||||
|
return self.format (os)
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
StatsPage.py
|
||||||
|
|
||||||
|
Copyright 2009-2014 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The appserver stats page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import TemplatedPage
|
||||||
|
import asyncdns
|
||||||
|
import ipinfo
|
||||||
|
|
||||||
|
|
||||||
|
class StatsPage (TemplatedPage.TemplatedPage):
|
||||||
|
""" Output some statistics. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = 'application/xhtml+xml; charset=UTF-8'
|
||||||
|
FORMATTER = 'html'
|
||||||
|
|
||||||
|
def index (self, **kwargs):
|
||||||
|
""" Output stats. """
|
||||||
|
|
||||||
|
backends = int (BaseSearcher.sql_get ("SELECT count (*) from pg_stat_activity"))
|
||||||
|
active_backends = int (BaseSearcher.sql_get (
|
||||||
|
"SELECT count (*) - 1 from pg_stat_activity where current_query !~ '^<IDLE>'"))
|
||||||
|
|
||||||
|
ipsessions = list (cherrypy.tools.rate_limiter.cache.values ()) # pylint: disable=E1101
|
||||||
|
|
||||||
|
adns = asyncdns.AsyncDNS ()
|
||||||
|
|
||||||
|
# blocked IPs
|
||||||
|
blocked = sorted ([s for s in ipsessions if s.get ('blocked', 0) >= 2],
|
||||||
|
key = lambda s: s.ips.sort_key ())
|
||||||
|
if 'resolve' in kwargs:
|
||||||
|
for d in blocked:
|
||||||
|
if d.ips.ipinfo is None:
|
||||||
|
d.ips.ipinfo = ipinfo.IPInfo (adns, d.ips.get_ip_to_block ())
|
||||||
|
|
||||||
|
# active IPs
|
||||||
|
active = sorted ([s for s in ipsessions if s.get ('active', False)],
|
||||||
|
key = lambda s: s.ips.sort_key ())
|
||||||
|
|
||||||
|
# busiest IPs
|
||||||
|
busiest = sorted ([s for s in active if s.get ('blocked', 0) < 2],
|
||||||
|
key = lambda x: -x.get ('rhits'))[:10]
|
||||||
|
if 'resolve' in kwargs:
|
||||||
|
for d in busiest:
|
||||||
|
if d.ips.ipinfo is None:
|
||||||
|
d.ips.ipinfo = ipinfo.IPInfo (adns, d.ips.get_ip_to_block ())
|
||||||
|
|
||||||
|
# IPs with most sessions
|
||||||
|
most_sessions = sorted ([s for s in active
|
||||||
|
if not s.ips.whitelisted and len (s.sessions) > 1],
|
||||||
|
key = lambda s: -len (s.sessions))[:10]
|
||||||
|
if 'resolve' in kwargs:
|
||||||
|
for d in most_sessions:
|
||||||
|
if d.ips.ipinfo is None:
|
||||||
|
d.ips.ipinfo = ipinfo.IPInfo (adns, d.ips.get_ip_to_block ())
|
||||||
|
|
||||||
|
adns.wait ()
|
||||||
|
adns.cancel ()
|
||||||
|
|
||||||
|
return self.output ('stats',
|
||||||
|
active = active,
|
||||||
|
blocked = blocked,
|
||||||
|
busiest = busiest,
|
||||||
|
most_sessions = most_sessions,
|
||||||
|
resolve = 'resolve' in kwargs,
|
||||||
|
rl = cherrypy.tools.rate_limiter, # pylint: disable=E1101
|
||||||
|
backends = backends,
|
||||||
|
active_backends = active_backends)
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
SuggestionsPage.py
|
||||||
|
|
||||||
|
Copyright 2012 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
The search suggestions page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
from libgutenberg.GutenbergDatabase import DatabaseError
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
import Page
|
||||||
|
|
||||||
|
class Suggestions (Page.Page):
|
||||||
|
""" Output the search suggestions page. """
|
||||||
|
|
||||||
|
sql_searcher = BaseSearcher.SQLSearcher ()
|
||||||
|
|
||||||
|
def index (self, **dummy_kwargs):
|
||||||
|
""" Output the suggestions page. """
|
||||||
|
|
||||||
|
cherrypy.request.params['format'] = 'json' # override user
|
||||||
|
|
||||||
|
os = BaseSearcher.OpenSearch ()
|
||||||
|
os.sort_order = 'nentry'
|
||||||
|
os.start_index = 1
|
||||||
|
os.items_per_page = 5
|
||||||
|
|
||||||
|
if os.format != 'json':
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Unknown format.')
|
||||||
|
|
||||||
|
if len (os.query) == 0:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. No query.')
|
||||||
|
|
||||||
|
last_word = os.query.split ()[-1]
|
||||||
|
if len (last_word) < 4:
|
||||||
|
raise cherrypy.HTTPError (400, 'Bad Request. Query too short.')
|
||||||
|
|
||||||
|
# ok. request looks sane. process it
|
||||||
|
|
||||||
|
os.log_request ('suggestions')
|
||||||
|
|
||||||
|
os.f_format_title = os.format_suggestion
|
||||||
|
os.f_format_subtitle = os.format_none
|
||||||
|
os.f_format_extra = os.format_none
|
||||||
|
os.f_format_url = os.format_none
|
||||||
|
os.f_format_thumb_url = os.format_none
|
||||||
|
os.f_format_icon = os.format_none
|
||||||
|
|
||||||
|
sql = BaseSearcher.SQLStatement ()
|
||||||
|
|
||||||
|
# prepare inner query
|
||||||
|
sql.query = 'SELECT tsvec'
|
||||||
|
sql.from_ = ('books', )
|
||||||
|
sql.fulltext ('books.tsvec', os.query)
|
||||||
|
inner_sql_query = self.sql_searcher.mogrify (os, sql)
|
||||||
|
|
||||||
|
sql.query = "SELECT substr (word, 2) AS title FROM ts_stat ( %(inner)s )"
|
||||||
|
sql.from_ = ()
|
||||||
|
sql.params['inner'] = inner_sql_query
|
||||||
|
sql.where = ["word ~* %(re_word)s"]
|
||||||
|
sql.params['re_word'] = '^0' + last_word
|
||||||
|
|
||||||
|
try:
|
||||||
|
os = self.sql_searcher.search (os, sql)
|
||||||
|
except DatabaseError as what:
|
||||||
|
cherrypy.log ("SQL Error: " + str (what),
|
||||||
|
context = 'REQUEST', severity = logging.ERROR)
|
||||||
|
raise cherrypy.HTTPError (500, 'Internal Server Error.')
|
||||||
|
|
||||||
|
os.template = os.page = 'results'
|
||||||
|
os.finalize ()
|
||||||
|
|
||||||
|
return self.format (os)
|
|
@ -0,0 +1,272 @@
|
||||||
|
""" Locales supported by 3rd party apps. Generated file. Do not edit! """
|
||||||
|
|
||||||
|
|
||||||
|
FB_LANGS = set ((
|
||||||
|
'af_ZA',
|
||||||
|
'ar_AR',
|
||||||
|
'az_AZ',
|
||||||
|
'be_BY',
|
||||||
|
'bg_BG',
|
||||||
|
'bn_IN',
|
||||||
|
'bs_BA',
|
||||||
|
'ca_ES',
|
||||||
|
'cs_CZ',
|
||||||
|
'cx_PH',
|
||||||
|
'cy_GB',
|
||||||
|
'da_DK',
|
||||||
|
'de_DE',
|
||||||
|
'el_GR',
|
||||||
|
'en_GB',
|
||||||
|
'en_PI',
|
||||||
|
'en_UD',
|
||||||
|
'en_US',
|
||||||
|
'eo_EO',
|
||||||
|
'es_ES',
|
||||||
|
'es_LA',
|
||||||
|
'et_EE',
|
||||||
|
'eu_ES',
|
||||||
|
'fa_IR',
|
||||||
|
'fb_LT',
|
||||||
|
'fi_FI',
|
||||||
|
'fo_FO',
|
||||||
|
'fr_CA',
|
||||||
|
'fr_FR',
|
||||||
|
'fy_NL',
|
||||||
|
'ga_IE',
|
||||||
|
'gl_ES',
|
||||||
|
'gn_PY',
|
||||||
|
'gu_IN',
|
||||||
|
'he_IL',
|
||||||
|
'hi_IN',
|
||||||
|
'hr_HR',
|
||||||
|
'hu_HU',
|
||||||
|
'hy_AM',
|
||||||
|
'id_ID',
|
||||||
|
'is_IS',
|
||||||
|
'it_IT',
|
||||||
|
'ja_JP',
|
||||||
|
'ja_KS',
|
||||||
|
'jv_ID',
|
||||||
|
'ka_GE',
|
||||||
|
'kk_KZ',
|
||||||
|
'km_KH',
|
||||||
|
'kn_IN',
|
||||||
|
'ko_KR',
|
||||||
|
'ku_TR',
|
||||||
|
'la_VA',
|
||||||
|
'lt_LT',
|
||||||
|
'lv_LV',
|
||||||
|
'mk_MK',
|
||||||
|
'ml_IN',
|
||||||
|
'mn_MN',
|
||||||
|
'mr_IN',
|
||||||
|
'ms_MY',
|
||||||
|
'nb_NO',
|
||||||
|
'ne_NP',
|
||||||
|
'nl_NL',
|
||||||
|
'nn_NO',
|
||||||
|
'pa_IN',
|
||||||
|
'pl_PL',
|
||||||
|
'ps_AF',
|
||||||
|
'pt_BR',
|
||||||
|
'pt_PT',
|
||||||
|
'ro_RO',
|
||||||
|
'ru_RU',
|
||||||
|
'si_LK',
|
||||||
|
'sk_SK',
|
||||||
|
'sl_SI',
|
||||||
|
'sq_AL',
|
||||||
|
'sr_RS',
|
||||||
|
'sv_SE',
|
||||||
|
'sw_KE',
|
||||||
|
'ta_IN',
|
||||||
|
'te_IN',
|
||||||
|
'tg_TJ',
|
||||||
|
'th_TH',
|
||||||
|
'tl_PH',
|
||||||
|
'tr_TR',
|
||||||
|
'uk_UA',
|
||||||
|
'ur_PK',
|
||||||
|
'uz_UZ',
|
||||||
|
'vi_VN',
|
||||||
|
'zh_CN',
|
||||||
|
'zh_HK',
|
||||||
|
'zh_TW',
|
||||||
|
))
|
||||||
|
|
||||||
|
FLATTR_LANGS = set ((
|
||||||
|
'ar_DZ',
|
||||||
|
'be_BY',
|
||||||
|
'bg_BG',
|
||||||
|
'br_FR',
|
||||||
|
'ca_ES',
|
||||||
|
'cs_CZ',
|
||||||
|
'da_DK',
|
||||||
|
'de_DE',
|
||||||
|
'el_GR',
|
||||||
|
'en_GB',
|
||||||
|
'eo_EO',
|
||||||
|
'es_ES',
|
||||||
|
'es_GL',
|
||||||
|
'et_EE',
|
||||||
|
'fa_FA',
|
||||||
|
'fi_FI',
|
||||||
|
'fr_FR',
|
||||||
|
'ga_IE',
|
||||||
|
'hi_IN',
|
||||||
|
'hr_HR',
|
||||||
|
'hu_HU',
|
||||||
|
'in_ID',
|
||||||
|
'is_IS',
|
||||||
|
'it_IT',
|
||||||
|
'iw_IL',
|
||||||
|
'ja_JP',
|
||||||
|
'ko_KR',
|
||||||
|
'lt_LT',
|
||||||
|
'lv_LV',
|
||||||
|
'mk_MK',
|
||||||
|
'ms_MY',
|
||||||
|
'mt_MT',
|
||||||
|
'nl_NL',
|
||||||
|
'nn_NO',
|
||||||
|
'no_NO',
|
||||||
|
'pl_PL',
|
||||||
|
'pt_PT',
|
||||||
|
'ro_RO',
|
||||||
|
'ru_RU',
|
||||||
|
'sk_SK',
|
||||||
|
'sl_SI',
|
||||||
|
'sq_AL',
|
||||||
|
'sr_RS',
|
||||||
|
'sv_SE',
|
||||||
|
'th_TH',
|
||||||
|
'tr_TR',
|
||||||
|
'uk_UA',
|
||||||
|
'vi_VN',
|
||||||
|
'zh_CN',
|
||||||
|
))
|
||||||
|
|
||||||
|
GOOGLE_LANGS = set ((
|
||||||
|
'af',
|
||||||
|
'am',
|
||||||
|
'ar',
|
||||||
|
'eu',
|
||||||
|
'bn',
|
||||||
|
'bg',
|
||||||
|
'ca',
|
||||||
|
'zh_HK',
|
||||||
|
'zh_CN',
|
||||||
|
'zh_TW',
|
||||||
|
'hr',
|
||||||
|
'cs',
|
||||||
|
'da',
|
||||||
|
'nl',
|
||||||
|
'en_GB',
|
||||||
|
'en_US',
|
||||||
|
'et',
|
||||||
|
'fil',
|
||||||
|
'fi',
|
||||||
|
'fr',
|
||||||
|
'fr_CA',
|
||||||
|
'gl',
|
||||||
|
'de',
|
||||||
|
'el',
|
||||||
|
'gu',
|
||||||
|
'iw',
|
||||||
|
'hi',
|
||||||
|
'hu',
|
||||||
|
'is',
|
||||||
|
'id',
|
||||||
|
'it',
|
||||||
|
'ja',
|
||||||
|
'kn',
|
||||||
|
'ko',
|
||||||
|
'lv',
|
||||||
|
'lt',
|
||||||
|
'ms',
|
||||||
|
'ml',
|
||||||
|
'mr',
|
||||||
|
'no',
|
||||||
|
'fa',
|
||||||
|
'pl',
|
||||||
|
'pt_BR',
|
||||||
|
'pt_PT',
|
||||||
|
'ro',
|
||||||
|
'ru',
|
||||||
|
'sr',
|
||||||
|
'sk',
|
||||||
|
'sl',
|
||||||
|
'es',
|
||||||
|
'es_419',
|
||||||
|
'sw',
|
||||||
|
'sv',
|
||||||
|
'ta',
|
||||||
|
'te',
|
||||||
|
'th',
|
||||||
|
'tr',
|
||||||
|
'uk',
|
||||||
|
'ur',
|
||||||
|
'vi',
|
||||||
|
'zu',
|
||||||
|
))
|
||||||
|
|
||||||
|
TWITTER_LANGS = set ((
|
||||||
|
'fr',
|
||||||
|
'en',
|
||||||
|
'ar',
|
||||||
|
'ja',
|
||||||
|
'es',
|
||||||
|
'de',
|
||||||
|
'it',
|
||||||
|
'id',
|
||||||
|
'pt',
|
||||||
|
'ko',
|
||||||
|
'tr',
|
||||||
|
'ru',
|
||||||
|
'nl',
|
||||||
|
'fil',
|
||||||
|
'msa',
|
||||||
|
'zh-tw',
|
||||||
|
'zh-cn',
|
||||||
|
'hi',
|
||||||
|
'no',
|
||||||
|
'sv',
|
||||||
|
'fi',
|
||||||
|
'da',
|
||||||
|
'pl',
|
||||||
|
'hu',
|
||||||
|
'fa',
|
||||||
|
'he',
|
||||||
|
'th',
|
||||||
|
'uk',
|
||||||
|
'cs',
|
||||||
|
'ro',
|
||||||
|
'en-gb',
|
||||||
|
'vi',
|
||||||
|
'bn',
|
||||||
|
))
|
||||||
|
|
||||||
|
PAYPAL_LANGS = set ((
|
||||||
|
'da_DK',
|
||||||
|
'de_DE',
|
||||||
|
'en_GB',
|
||||||
|
'en_US',
|
||||||
|
'es_ES',
|
||||||
|
'fr_CA',
|
||||||
|
'fr_FR',
|
||||||
|
'he_IL',
|
||||||
|
'id_ID',
|
||||||
|
'it_IT',
|
||||||
|
'ja_JP',
|
||||||
|
'nl_NL',
|
||||||
|
'pl_PL',
|
||||||
|
'pt_BR',
|
||||||
|
'pt_PT',
|
||||||
|
'ru_RU',
|
||||||
|
'sv_SE',
|
||||||
|
'tr_TR',
|
||||||
|
'zh_CN',
|
||||||
|
'zh_HK',
|
||||||
|
'zh_TW',
|
||||||
|
))
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: iso-8859-1 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
TemplatedPage.py
|
||||||
|
|
||||||
|
Copyright 2013 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Output a Genshi page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import genshi.output
|
||||||
|
import genshi.template
|
||||||
|
from genshi.core import _ensure
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
import Formatters
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatedPage (object):
|
||||||
|
""" Output a page from a genshi template. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = 'application/xml; charset=UTF-8'
|
||||||
|
FORMATTER = 'xml'
|
||||||
|
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
""" Override to get a different serializer. """
|
||||||
|
|
||||||
|
return genshi.output.XMLSerializer (strip_whitespace = False)
|
||||||
|
|
||||||
|
|
||||||
|
def output (self, template, **kwargs):
|
||||||
|
""" Output the page. """
|
||||||
|
|
||||||
|
# Send HTTP content-type header.
|
||||||
|
cherrypy.response.headers['Content-Type'] = self.CONTENT_TYPE
|
||||||
|
|
||||||
|
template = Formatters.formatters[self.FORMATTER].templates[template]
|
||||||
|
ctxt = genshi.template.Context (cherrypy = cherrypy, bs = BaseSearcher, **kwargs)
|
||||||
|
|
||||||
|
stream = template.stream
|
||||||
|
for filter_ in template.filters:
|
||||||
|
stream = filter_ (iter (stream), ctxt)
|
||||||
|
|
||||||
|
serializer = self.get_serializer ()
|
||||||
|
|
||||||
|
return genshi.output.encode (serializer (
|
||||||
|
_ensure (genshi.Stream (stream))), encoding = 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatedPageXHTML (TemplatedPage):
|
||||||
|
""" Output a page from a genshi template. """
|
||||||
|
|
||||||
|
CONTENT_TYPE = 'text/html; charset=UTF-8'
|
||||||
|
FORMATTER = 'html'
|
||||||
|
DOCTYPE = ('html',
|
||||||
|
'-//W3C//DTD XHTML 1.0 Strict//EN',
|
||||||
|
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd')
|
||||||
|
|
||||||
|
def get_serializer (self):
|
||||||
|
return genshi.output.XHTMLSerializer (doctype = self.DOCTYPE, strip_whitespace = False)
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Timer.py
|
||||||
|
|
||||||
|
Copyright 2010-2013 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
A cron-like process that runs at fixed intervals.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
import Timer
|
||||||
|
cherrypy.process.plugins.Timer = TimerPlugin
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
|
||||||
|
import BaseSearcher
|
||||||
|
|
||||||
|
|
||||||
|
class TimerPlugin (cherrypy.process.plugins.Monitor):
|
||||||
|
""" Plugin to start the timer thread.
|
||||||
|
|
||||||
|
We cannot start any threads before daemonizing,
|
||||||
|
so we must start the timer thread by this plugin.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__ (self, bus):
|
||||||
|
# interval in seconds
|
||||||
|
frequency = 300
|
||||||
|
super (TimerPlugin, self).__init__ (bus, self.tick, frequency)
|
||||||
|
self.name = 'timer'
|
||||||
|
|
||||||
|
def start (self):
|
||||||
|
super (TimerPlugin, self).start ()
|
||||||
|
self.tick ()
|
||||||
|
start.priority = 80
|
||||||
|
|
||||||
|
def tick (self):
|
||||||
|
""" Do things here. """
|
||||||
|
|
||||||
|
try:
|
||||||
|
BaseSearcher.books_in_archive = BaseSearcher.sql_get ('select count (*) from books')
|
||||||
|
except:
|
||||||
|
pass
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
UserAgent.py
|
||||||
|
|
||||||
|
Copyright 2010 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Mobile Browser Detection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
UA = "iPhone MIDP BlackBerry Android Mobile webOS Blazer Kindle nook"
|
|
@ -0,0 +1,2 @@
|
||||||
|
""" Package """
|
||||||
|
name = "autocat3"
|
|
@ -0,0 +1,200 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
asyncdns.py
|
||||||
|
|
||||||
|
Copyright 2013-14 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Higher level interface to the GNU asynchronous DNS library.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import adns
|
||||||
|
|
||||||
|
# pass this to __init__ to use Google Public DNS
|
||||||
|
RESOLV_CONF = 'nameserver 8.8.8.8'
|
||||||
|
|
||||||
|
# http://www.ietf.org/rfc/rfc1035.txt Domain Names
|
||||||
|
# http://www.ietf.org/rfc/rfc3490.txt IDNA
|
||||||
|
# http://www.ietf.org/rfc/rfc3492.txt Punycode
|
||||||
|
|
||||||
|
class AsyncDNS (object):
|
||||||
|
""" An asynchronous DNS resolver. """
|
||||||
|
|
||||||
|
def __init__ (self, resolv_conf = None):
|
||||||
|
if resolv_conf:
|
||||||
|
self.resolver = adns.init (
|
||||||
|
adns.iflags.noautosys + adns.iflags.noerrprint,
|
||||||
|
sys.stderr, # FIXME: adns version 1.2.2 will allow keyword params
|
||||||
|
resolv_conf)
|
||||||
|
else:
|
||||||
|
self.resolver = adns.init (
|
||||||
|
adns.iflags.noautosys + adns.iflags.noerrprint)
|
||||||
|
|
||||||
|
self._queries = {} # keeps query objects alive
|
||||||
|
|
||||||
|
def query (self, query, callback, rr = adns.rr.A):
|
||||||
|
""" Queue a query.
|
||||||
|
|
||||||
|
:param query: the query string (may contain unicode characters)
|
||||||
|
:param callback: function taking a tuple of answers
|
||||||
|
:param rr: the query rr type code
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if rr not in (adns.rr.PTR, adns.rr.PTRraw):
|
||||||
|
query = self.encode (query)
|
||||||
|
if rr in (adns.rr.PTR, adns.rr.PTRraw):
|
||||||
|
self._queries [self.resolver.submit_reverse (query, rr)] = callback, rr
|
||||||
|
else:
|
||||||
|
self._queries [self.resolver.submit (query, rr)] = callback, rr
|
||||||
|
|
||||||
|
def query_dnsbl (self, query, zone, callback, rr = adns.rr.A):
|
||||||
|
""" Queue a reverse dnsbl-type query. """
|
||||||
|
self._queries [self.resolver.submit_reverse_any (query, zone, rr)] = callback, rr
|
||||||
|
|
||||||
|
def done (self):
|
||||||
|
""" Are all queued queries answered? """
|
||||||
|
return not self._queries
|
||||||
|
|
||||||
|
def wait (self, timeout = 10):
|
||||||
|
""" Wait for the queries to complete. """
|
||||||
|
|
||||||
|
timeout += time.time ()
|
||||||
|
while self._queries and time.time () < timeout:
|
||||||
|
for q in self.resolver.completed (1):
|
||||||
|
answer = q.check ()
|
||||||
|
callback, rr = self._queries[q]
|
||||||
|
del self._queries[q]
|
||||||
|
|
||||||
|
# print (answer)
|
||||||
|
|
||||||
|
a0 = answer[0]
|
||||||
|
if a0 == 0:
|
||||||
|
callback (self.decode_answer (rr, answer[3]))
|
||||||
|
elif a0 == 101 and rr == adns.rr.A:
|
||||||
|
# got CNAME, wanted A: resubmit
|
||||||
|
self.query (answer[1], callback, rr)
|
||||||
|
# else
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
|
def decode_answer (self, rr, answers):
|
||||||
|
""" Decode the answer to unicode.
|
||||||
|
|
||||||
|
Supports only some rr types. You may override this to support
|
||||||
|
some more.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if rr in (adns.rr.A, adns.rr.TXT):
|
||||||
|
# A records are ip addresses that need no decoding.
|
||||||
|
# TXT records may be anything, even binary data,
|
||||||
|
# so leave decoding to the caller.
|
||||||
|
return answers
|
||||||
|
|
||||||
|
if rr in (adns.rr.PTR, adns.rr.PTRraw, adns.rr.CNAME, adns.rr.NSraw):
|
||||||
|
return [ self.decode (host) for host in answers ]
|
||||||
|
|
||||||
|
if rr == adns.rr.MXraw:
|
||||||
|
return [ (prio, self.decode (host)) for (prio, host) in answers ]
|
||||||
|
|
||||||
|
if rr == adns.rr.SRVraw:
|
||||||
|
return [ (prio, weight, port, self.decode (host))
|
||||||
|
for (prio, weight, port, host) in answers ]
|
||||||
|
|
||||||
|
if rr in (adns.rr.SOA, adns.rr.SOAraw):
|
||||||
|
return [ (self.decode (mname), self.decode (rname),
|
||||||
|
serial, refresh, retry, expire, minimum)
|
||||||
|
for (mname, rname, serial, refresh,
|
||||||
|
retry, expire, minimum) in answers ]
|
||||||
|
|
||||||
|
# unsupported HINFO, RP, RPraw, NS, SRV, MX
|
||||||
|
|
||||||
|
return answers
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode (query):
|
||||||
|
""" Encode a unicode query to idna.
|
||||||
|
|
||||||
|
Result will still be of type unicode/str.
|
||||||
|
"""
|
||||||
|
return query.encode ('idna').decode ('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode (answer):
|
||||||
|
""" Decode an answer to unicode. """
|
||||||
|
try:
|
||||||
|
return answer.decode ('idna')
|
||||||
|
except ValueError:
|
||||||
|
return answer.decode ('ascii', 'replace')
|
||||||
|
|
||||||
|
|
||||||
|
def cancel (self):
|
||||||
|
""" Cancel all pending queries. """
|
||||||
|
|
||||||
|
for q in self._queries.keys ():
|
||||||
|
q.cancel ()
|
||||||
|
self._queries.clear ()
|
||||||
|
|
||||||
|
|
||||||
|
def bulk_query (self, query_dict, rr):
|
||||||
|
""" Bulk lookup.
|
||||||
|
|
||||||
|
:param dict: on entry { query1: None, query2: None }
|
||||||
|
on exit { query1: (answer1, ), query2: (answer2a, answer2b) }
|
||||||
|
|
||||||
|
Note: you must call wait () after bulk_query () for the answers to appear
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def itemsetter (query):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
the dictionary under the right key. """
|
||||||
|
def g (answer):
|
||||||
|
""" Put the answer into the dictionary. """
|
||||||
|
query_dict[query] = answer
|
||||||
|
# print "put: " + answer
|
||||||
|
return g
|
||||||
|
|
||||||
|
for query in query_dict.keys ():
|
||||||
|
if query:
|
||||||
|
self.query (query, itemsetter (query), rr)
|
||||||
|
|
||||||
|
|
||||||
|
def bulk_query (dict_, rr = adns.rr.A, timeout = 10):
|
||||||
|
""" Perform bulk lookup. """
|
||||||
|
a = AsyncDNS ()
|
||||||
|
a.bulk_query (dict_, rr)
|
||||||
|
a.wait (timeout)
|
||||||
|
a.cancel ()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
queries = dict ()
|
||||||
|
for i in range (64, 64 + 32):
|
||||||
|
ip = '66.249.%d.42' % i # google assigned netblock
|
||||||
|
queries[ip] = None
|
||||||
|
|
||||||
|
bulk_query (queries, adns.rr.PTR)
|
||||||
|
|
||||||
|
ipset = netaddr.IPSet ()
|
||||||
|
for ip in sorted (queries):
|
||||||
|
if queries[ip] and 'proxy' in queries[ip][0]:
|
||||||
|
print (ip)
|
||||||
|
ipset.add (ip + '/24')
|
||||||
|
|
||||||
|
for cidr in ipset.iter_cidrs ():
|
||||||
|
print (cidr)
|
|
@ -0,0 +1,341 @@
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Internationalization and Localization for CherryPy
|
||||||
|
|
||||||
|
**Tested with CherryPy 3.1.2**
|
||||||
|
|
||||||
|
This tool provides locales and loads translations based on the
|
||||||
|
HTTP-ACCEPT-LANGUAGE header. If no header is send or the given language
|
||||||
|
is not supported by the application, it falls back to
|
||||||
|
`tools.I18nTool.default`. Set `default` to the native language used in your
|
||||||
|
code for strings, so you must not provide a .mo file for it.
|
||||||
|
|
||||||
|
The tool uses `babel<http://babel.edgewall.org>`_ for localization and
|
||||||
|
handling translations. Within your Python code you can use four functions
|
||||||
|
defined in this module and the loaded locale provided as
|
||||||
|
`cherrypy.response.i18n.locale`.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
from i18n_tool import ugettext as _, ungettext
|
||||||
|
|
||||||
|
class MyController(object):
|
||||||
|
@cherrypy.expose
|
||||||
|
def index(self):
|
||||||
|
loc = cherrypy.response.i18n.locale
|
||||||
|
s1 = _('Translateable string')
|
||||||
|
s2 = ungettext('There is one string.',
|
||||||
|
'There are more strings.', 2)
|
||||||
|
return '<br />'.join([s1, s2, loc.display_name])
|
||||||
|
|
||||||
|
If you have code (e.g. database models) that is executed before the response
|
||||||
|
object is available, use the *_lazy functions to mark the strings
|
||||||
|
translateable. They will be translated later on, when the text is used (and
|
||||||
|
hopefully the response object is available then).
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
from i18n_tool import ugettext_lazy
|
||||||
|
|
||||||
|
class Model:
|
||||||
|
def __init__(self):
|
||||||
|
name = ugettext_lazy('Name of the model')
|
||||||
|
|
||||||
|
For your templates read the documentation of your template engine how to
|
||||||
|
integrate babel with it. I think `Genshi<http://genshi.edgewall.org>`_ and
|
||||||
|
`Jinja 2<http://jinja.pocoo.org`_ support it out of the box.
|
||||||
|
|
||||||
|
|
||||||
|
Settings for the CherryPy configuration::
|
||||||
|
|
||||||
|
[/]
|
||||||
|
tools.I18nTool.on = True
|
||||||
|
tools.I18nTool.default = Your language with territory (e.g. 'en_US')
|
||||||
|
tools.I18nTool.mo_dir = Directory holding the locale directories
|
||||||
|
tools.I18nTool.domain = Your gettext domain (e.g. application name)
|
||||||
|
|
||||||
|
The mo_dir must contain subdirectories named with the language prefix
|
||||||
|
for all translations, containing a LC_MESSAGES dir with the compiled
|
||||||
|
catalog file in it.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
[/]
|
||||||
|
tools.I18nTool.on = True
|
||||||
|
tools.I18nTool.default = 'en_US'
|
||||||
|
tools.I18nTool.mo_dir = '/home/user/web/myapp/i18n'
|
||||||
|
tools.I18nTool.domain = 'myapp'
|
||||||
|
|
||||||
|
Now the tool will look for a file called myapp.mo in
|
||||||
|
/home/user/web/myapp/i18n/en/LC_MESSACES/
|
||||||
|
or generic: <mo_dir>/<language>/LC_MESSAGES/<domain>.mo
|
||||||
|
|
||||||
|
That's it.
|
||||||
|
|
||||||
|
:License: BSD
|
||||||
|
:Author: Thorsten Weimann <thorsten.weimann (at) gmx (dot) net>
|
||||||
|
:Date: 2010-02-08
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
import six
|
||||||
|
from babel.core import Locale, UnknownLocaleError
|
||||||
|
from babel.support import Translations, LazyProxy
|
||||||
|
|
||||||
|
# Cache for Translations objects
|
||||||
|
_trans_cache = {}
|
||||||
|
|
||||||
|
class Struct (object):
|
||||||
|
""" Empty class to pin attributes on later. """
|
||||||
|
pass
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
# Public translation functions
|
||||||
|
def ugettext(message):
|
||||||
|
"""Standard translation function. You can use it in all your exposed
|
||||||
|
methods and everywhere where the response object is available.
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
message : Unicode
|
||||||
|
The message to translate.
|
||||||
|
|
||||||
|
:returns: The translated message.
|
||||||
|
:rtype: Unicode
|
||||||
|
"""
|
||||||
|
if message:
|
||||||
|
return cherrypy.response.i18n.trans.ugettext(message)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def ugettext_lazy(message):
|
||||||
|
"""Like ugettext, but lazy.
|
||||||
|
|
||||||
|
:returns: A proxy for the translation object.
|
||||||
|
:rtype: LazyProxy
|
||||||
|
"""
|
||||||
|
def get_translation():
|
||||||
|
return cherrypy.response.i18n.trans.ugettext(message)
|
||||||
|
return LazyProxy(get_translation)
|
||||||
|
|
||||||
|
def ungettext(singular, plural, num):
|
||||||
|
"""Like ugettext, but considers plural forms.
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
singular : Unicode
|
||||||
|
The message to translate in singular form.
|
||||||
|
plural : Unicode
|
||||||
|
The message to translate in plural form.
|
||||||
|
num : Integer
|
||||||
|
Number to apply the plural formula on. If num is 1 or no
|
||||||
|
translation is found, singular is returned.
|
||||||
|
|
||||||
|
:returns: The translated message as singular or plural.
|
||||||
|
:rtype: Unicode
|
||||||
|
"""
|
||||||
|
return cherrypy.response.i18n.trans.ungettext(singular, plural, num)
|
||||||
|
|
||||||
|
def ungettext_lazy(singular, plural, num):
|
||||||
|
"""Like ungettext, but lazy.
|
||||||
|
|
||||||
|
:returns: A proxy for the translation object.
|
||||||
|
:rtype: LazyProxy
|
||||||
|
"""
|
||||||
|
def get_translation():
|
||||||
|
return cherrypy.response.i18n.trans.ungettext(singular, plural, num)
|
||||||
|
return LazyProxy(get_translation)
|
||||||
|
|
||||||
|
else: # PY3
|
||||||
|
|
||||||
|
# Public translation functions
|
||||||
|
def ugettext(message):
|
||||||
|
"""Standard translation function. You can use it in all your exposed
|
||||||
|
methods and everywhere where the response object is available.
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
message : Unicode
|
||||||
|
The message to translate.
|
||||||
|
|
||||||
|
:returns: The translated message.
|
||||||
|
:rtype: Unicode
|
||||||
|
"""
|
||||||
|
if message:
|
||||||
|
return cherrypy.response.i18n.trans.gettext(message)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def ugettext_lazy(message):
|
||||||
|
"""Like ugettext, but lazy.
|
||||||
|
|
||||||
|
:returns: A proxy for the translation object.
|
||||||
|
:rtype: LazyProxy
|
||||||
|
"""
|
||||||
|
def get_translation():
|
||||||
|
return cherrypy.response.i18n.trans.gettext(message)
|
||||||
|
return LazyProxy(get_translation)
|
||||||
|
|
||||||
|
def ungettext(singular, plural, num):
|
||||||
|
"""Like ugettext, but considers plural forms.
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
singular : Unicode
|
||||||
|
The message to translate in singular form.
|
||||||
|
plural : Unicode
|
||||||
|
The message to translate in plural form.
|
||||||
|
num : Integer
|
||||||
|
Number to apply the plural formula on. If num is 1 or no
|
||||||
|
translation is found, singular is returned.
|
||||||
|
|
||||||
|
:returns: The translated message as singular or plural.
|
||||||
|
:rtype: Unicode
|
||||||
|
"""
|
||||||
|
return cherrypy.response.i18n.trans.ngettext(singular, plural, num)
|
||||||
|
|
||||||
|
def ungettext_lazy(singular, plural, num):
|
||||||
|
"""Like ungettext, but lazy.
|
||||||
|
|
||||||
|
:returns: A proxy for the translation object.
|
||||||
|
:rtype: LazyProxy
|
||||||
|
"""
|
||||||
|
def get_translation():
|
||||||
|
return cherrypy.response.i18n.trans.ngettext(singular, plural, num)
|
||||||
|
return LazyProxy(get_translation)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def load_translation(languages, dirname, domain, default):
|
||||||
|
"""Loads the first existing translations for known locale and saves the
|
||||||
|
`Lang` object in a global cache for faster lookup on the next request.
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
langs : List
|
||||||
|
List of languages as returned by `parse_accept_language_header`.
|
||||||
|
dirname : String
|
||||||
|
Directory of the translations (`tools.I18nTool.mo_dir`).
|
||||||
|
domain : String
|
||||||
|
Gettext domain of the catalog (`tools.I18nTool.domain`).
|
||||||
|
|
||||||
|
:returns: Lang object with two attributes (Lang.trans = the translations
|
||||||
|
object, Lang.locale = the corresponding Locale object).
|
||||||
|
:rtype: Lang
|
||||||
|
:raises: ImproperlyConfigured if no locale where known.
|
||||||
|
"""
|
||||||
|
|
||||||
|
res = Struct ()
|
||||||
|
res.trans = gettext.NullTranslations ()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# use the preferred locale for date formatting
|
||||||
|
# even if we have no translation for it
|
||||||
|
res.locale = Locale.parse (languages [0])
|
||||||
|
except (IndexError, ValueError, UnknownLocaleError):
|
||||||
|
res.locale = Locale.parse (default)
|
||||||
|
|
||||||
|
for language in languages:
|
||||||
|
try:
|
||||||
|
#cherrypy.log ("trying %s" % str (language),
|
||||||
|
# context = 'REQUEST', severity = logging.DEBUG)
|
||||||
|
|
||||||
|
locale = str (Locale.parse (language))
|
||||||
|
|
||||||
|
# cached ?
|
||||||
|
if (domain, locale) in _trans_cache:
|
||||||
|
res.trans = _trans_cache[(domain, locale)]
|
||||||
|
return res
|
||||||
|
|
||||||
|
# not cached
|
||||||
|
trans = Translations.load (dirname, locale, domain)
|
||||||
|
if isinstance (trans, Translations):
|
||||||
|
res.trans = _trans_cache [(domain, locale)] = trans
|
||||||
|
break
|
||||||
|
|
||||||
|
except (ValueError, UnknownLocaleError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_lang (mo_dir, default, domain):
|
||||||
|
"""Main function which will be invoked during the request by `I18nTool`.
|
||||||
|
If the SessionTool is on and has a lang key, this language get the
|
||||||
|
highest priority. Default language get the lowest priority.
|
||||||
|
The `Lang` object will be saved as `cherrypy.response.i18n` and the
|
||||||
|
language string will also saved as `cherrypy.session['_lang_']` (if
|
||||||
|
SessionTool is on).
|
||||||
|
|
||||||
|
:parameters:
|
||||||
|
mo_dir : String
|
||||||
|
`tools.I18nTool.mo_dir`
|
||||||
|
default : String
|
||||||
|
`tools.I18nTool.default`
|
||||||
|
domain : String
|
||||||
|
`tools.I18nTool.domain`
|
||||||
|
"""
|
||||||
|
|
||||||
|
# try explicit lang param, then session
|
||||||
|
|
||||||
|
lang = cherrypy.request.params.get ('lang', None)
|
||||||
|
|
||||||
|
if not lang:
|
||||||
|
try:
|
||||||
|
lang = cherrypy.session['_lang_']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if lang:
|
||||||
|
lang = lang.replace ('-', '_')
|
||||||
|
langs = (lang, )
|
||||||
|
else:
|
||||||
|
langs = cherrypy.request.headers.elements ('Accept-Language') or []
|
||||||
|
langs = [x.value.replace ('-', '_') for x in langs]
|
||||||
|
|
||||||
|
loc = load_translation (langs, mo_dir, domain, default)
|
||||||
|
cherrypy.response.i18n = loc
|
||||||
|
|
||||||
|
try:
|
||||||
|
cherrypy.session['_lang_'] = str (loc.locale)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def set_lang ():
|
||||||
|
"""Sets the Content-Language response header (if not already set) to the
|
||||||
|
language of `cherrypy.response.i18n.locale`.
|
||||||
|
"""
|
||||||
|
if 'Content-Language' not in cherrypy.response.headers:
|
||||||
|
if hasattr (cherrypy.response, 'i18n'):
|
||||||
|
cherrypy.response.headers['Content-Language'] = str (
|
||||||
|
cherrypy.response.i18n.locale)
|
||||||
|
|
||||||
|
|
||||||
|
class I18nTool (cherrypy.Tool):
|
||||||
|
"""Tool to integrate babel translations in CherryPy."""
|
||||||
|
|
||||||
|
def __init__ (self):
|
||||||
|
# cherrypy.Tool.__init__ (self)
|
||||||
|
self._name = 'I18nTool'
|
||||||
|
self._point = 'before_handler'
|
||||||
|
self.callable = get_lang
|
||||||
|
# Make sure, session tool (priority 50) is loaded before
|
||||||
|
self._priority = 100
|
||||||
|
|
||||||
|
def _setup (self):
|
||||||
|
c = cherrypy.request.config
|
||||||
|
if c.get ('tools.staticdir.on', False) or \
|
||||||
|
c.get ('tools.staticfile.on', False):
|
||||||
|
return
|
||||||
|
cherrypy.Tool._setup (self)
|
||||||
|
|
||||||
|
dirname = c.get ('tools.I18nTool.mo_dir', 'i18n')
|
||||||
|
default = c.get ('tools.I18nTool.default', 'en')[:2].lower ()
|
||||||
|
domain = c.get ('tools.I18nTool.domain', 'messages')
|
||||||
|
|
||||||
|
trans = Translations.load (dirname, default, domain)
|
||||||
|
if isinstance (trans, Translations):
|
||||||
|
_trans_cache [(domain, default)] = trans
|
||||||
|
else:
|
||||||
|
_trans_cache [(domain, default)] = gettext.NullTranslations ()
|
||||||
|
|
||||||
|
cherrypy.request.hooks.attach ('before_finalize', set_lang)
|
|
@ -0,0 +1,321 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- mode: python; indent-tabs-mode: nil; -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
ipinfo.py
|
||||||
|
|
||||||
|
Copyright 2013-14 by Marcello Perathoner
|
||||||
|
|
||||||
|
Distributable under the GNU General Public License Version 3 or newer.
|
||||||
|
|
||||||
|
Find information about an IP, eg. hostname, whois, DNS blocklists.
|
||||||
|
|
||||||
|
|
||||||
|
The Spamhaus Block List (SBL) Advisory is a database of IP
|
||||||
|
addresses from which Spamhaus does not recommend the acceptance of
|
||||||
|
electronic mail.
|
||||||
|
|
||||||
|
The Spamhaus Exploits Block List (XBL) is a realtime database
|
||||||
|
of IP addresses of hijacked PCs infected by illegal 3rd party
|
||||||
|
exploits, including open proxies (HTTP, socks, AnalogX, wingate,
|
||||||
|
etc), worms/viruses with built-in spam engines, and other types of
|
||||||
|
trojan-horse exploits.
|
||||||
|
|
||||||
|
The Spamhaus PBL is a DNSBL database of end-user IP address
|
||||||
|
ranges which should not be delivering unauthenticated SMTP email
|
||||||
|
to any Internet mail server except those provided for specifically
|
||||||
|
by an ISP for that customer's use. The PBL helps networks enforce
|
||||||
|
their Acceptable Use Policy for dynamic and non-MTA customer IP
|
||||||
|
ranges.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import asyncdns
|
||||||
|
|
||||||
|
# pylint: disable=R0903
|
||||||
|
|
||||||
|
class DNSBL (object):
|
||||||
|
""" Base class for DNS blocklists. """
|
||||||
|
|
||||||
|
zone = ''
|
||||||
|
blackhat_tags = {}
|
||||||
|
dialup_tags = {}
|
||||||
|
|
||||||
|
|
||||||
|
### TOR ###
|
||||||
|
# see:
|
||||||
|
# https://www.torproject.org/projects/tordnsel.html.en
|
||||||
|
# https://www.dan.me.uk/dnsbl
|
||||||
|
|
||||||
|
class TorProject (DNSBL):
|
||||||
|
""" A TOR exitnode list. """
|
||||||
|
|
||||||
|
# note: reverse IP of www.gutenberg.org:80
|
||||||
|
zone = '80.47.134.19.152.ip-port.exitlist.torproject.org'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'TOR',
|
||||||
|
}
|
||||||
|
|
||||||
|
class TorDanme (DNSBL):
|
||||||
|
""" A TOR exitnode list. """
|
||||||
|
|
||||||
|
zone = 'torexit.dan.me.uk'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.100': 'TOR',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### SPAMHAUS ###
|
||||||
|
# see: http://www.spamhaus.org/faq/answers.lasso?section=DNSBL%20Usage#202
|
||||||
|
|
||||||
|
class Spamhaus (DNSBL):
|
||||||
|
""" A DNS blocklist. """
|
||||||
|
|
||||||
|
zone = 'zen.spamhaus.org'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'SPAMHAUS_SBL',
|
||||||
|
'127.0.0.3': 'SPAMHAUS_SBL_CSS',
|
||||||
|
'127.0.0.4': 'SPAMHAUS_XBL_CBL',
|
||||||
|
}
|
||||||
|
dialup_tags = {
|
||||||
|
'127.0.0.10': 'SPAMHAUS_PBL_ISP',
|
||||||
|
'127.0.0.11': 'SPAMHAUS_PBL',
|
||||||
|
}
|
||||||
|
lookup = 'http://www.spamhaus.org/query/ip/{ip}'
|
||||||
|
|
||||||
|
|
||||||
|
### SORBS ###
|
||||||
|
# see: http://www.sorbs.net/using.shtml
|
||||||
|
|
||||||
|
class SORBS (DNSBL):
|
||||||
|
""" A DNS blocklist. """
|
||||||
|
|
||||||
|
zone = 'dnsbl.sorbs.net'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'SORBS_HTTP_PROXY',
|
||||||
|
'127.0.0.3': 'SORBS_SOCKS_PROXY',
|
||||||
|
'127.0.0.4': 'SORBS_MISC_PROXY',
|
||||||
|
'127.0.0.5': 'SORBS_SMTP_RELAY',
|
||||||
|
'127.0.0.6': 'SORBS_SPAMMER',
|
||||||
|
'127.0.0.7': 'SORBS_WEB', # formmail etc.
|
||||||
|
'127.0.0.8': 'SORBS_BLOCK',
|
||||||
|
'127.0.0.9': 'SORBS_ZOMBIE',
|
||||||
|
'127.0.0.11': 'SORBS_BADCONF',
|
||||||
|
'127.0.0.12': 'SORBS_NOMAIL',
|
||||||
|
}
|
||||||
|
dialup_tags = {
|
||||||
|
'127.0.0.10': 'SORBS_DUL',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### mailspike.net ###
|
||||||
|
# see: http://mailspike.net/usage.html
|
||||||
|
|
||||||
|
class MailSpike (DNSBL):
|
||||||
|
""" A DNS blocklist. """
|
||||||
|
|
||||||
|
zone = 'bl.mailspike.net'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'MAILSPIKE_DISTRIBUTED_SPAM',
|
||||||
|
'127.0.0.10': 'MAILSPIKE_WORST_REPUTATION',
|
||||||
|
'127.0.0.11': 'MAILSPIKE_VERY_BAD_REPUTATION',
|
||||||
|
'127.0.0.12': 'MAILSPIKE_BAD_REPUTATION',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### shlink.org ###
|
||||||
|
# see: http://shlink.org/
|
||||||
|
|
||||||
|
class BlShlink (DNSBL):
|
||||||
|
""" A DNS blocklist. """
|
||||||
|
|
||||||
|
zone = 'bl.shlink.org'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'SHLINK_SPAM_SENDER',
|
||||||
|
'127.0.0.4': 'SHLINK_SPAM_ORIGINATOR',
|
||||||
|
'127.0.0.5': 'SHLINK_POLICY_BLOCK',
|
||||||
|
'127.0.0.6': 'SHLINK_ATTACKER',
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynShlink (DNSBL):
|
||||||
|
""" A DNS dul list. """
|
||||||
|
|
||||||
|
zone = 'dyn.shlink.org'
|
||||||
|
dialup_tags = {
|
||||||
|
'127.0.0.3': 'SHLINK_DUL',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### barracudacentral.org ###
|
||||||
|
# see: http://www.barracudacentral.org/rbl/how-to-usee
|
||||||
|
|
||||||
|
class Barracuda (DNSBL):
|
||||||
|
""" A DNS blocklist. """
|
||||||
|
|
||||||
|
zone = 'b.barracudacentral.org'
|
||||||
|
blackhat_tags = {
|
||||||
|
'127.0.0.2': 'BARRACUDA_BLOCK',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
### SHADOWSERVER ###
|
||||||
|
# http://www.shadowserver.org/wiki/pmwiki.php/Services/IP-BGP
|
||||||
|
|
||||||
|
class ShadowServer (DNSBL):
|
||||||
|
""" A DNS-based whois service. """
|
||||||
|
|
||||||
|
zone = 'origin.asn.shadowserver.org'
|
||||||
|
peer_zone = 'peer.asn.shadowserver.org'
|
||||||
|
fields = 'asn cidr org2 country org1 org'.split ()
|
||||||
|
|
||||||
|
|
||||||
|
# TEAMCYMRU
|
||||||
|
# http://www.team-cymru.org/Services/ip-to-asn.html
|
||||||
|
|
||||||
|
class TeamCymru (DNSBL):
|
||||||
|
""" A DNS-based whois service. """
|
||||||
|
|
||||||
|
zone = 'origin.asn.cymru.com'
|
||||||
|
asn_zone = 'asn.cymru.com'
|
||||||
|
fields = 'asn cidr country registry date'.split ()
|
||||||
|
|
||||||
|
|
||||||
|
class IPInfo (object):
|
||||||
|
""" Holds DNSBL information for one IP. """
|
||||||
|
|
||||||
|
dnsbl = [ Spamhaus, SORBS, MailSpike, BlShlink, DynShlink, Barracuda,
|
||||||
|
TorProject, TorDanme ]
|
||||||
|
""" Which blocklists to consider. """
|
||||||
|
|
||||||
|
def __init__ (self, aresolver, ip):
|
||||||
|
self.hostname = None
|
||||||
|
self.whois = {}
|
||||||
|
self.blackhat_tags = set ()
|
||||||
|
self.dialup_tags = set ()
|
||||||
|
|
||||||
|
ip = str (ip)
|
||||||
|
rr = asyncdns.adns.rr
|
||||||
|
|
||||||
|
try:
|
||||||
|
aresolver.query (ip, self._hostnamesetter (), rr.PTR)
|
||||||
|
|
||||||
|
for dnsbl in self.dnsbl:
|
||||||
|
aresolver.query_dnsbl (ip, dnsbl.zone, self._tagsetter (dnsbl))
|
||||||
|
|
||||||
|
# ShadowServer seems down: March 2014
|
||||||
|
aresolver.query_dnsbl (ip, ShadowServer.zone, self._whoissetter_ss (), rr.TXT)
|
||||||
|
# aresolver.query_dnsbl (ip, TeamCymru.zone, self._whoissetter_tc (aresolver), rr.TXT)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tags (self):
|
||||||
|
""" All tags (bad and dialup). """
|
||||||
|
return self.blackhat_tags | self.dialup_tags
|
||||||
|
|
||||||
|
|
||||||
|
def is_blackhat (self):
|
||||||
|
""" Return true if this is probably a blackhat IP. """
|
||||||
|
return bool (self.blackhat_tags)
|
||||||
|
|
||||||
|
|
||||||
|
def is_dialup (self):
|
||||||
|
""" Test if this IP is a dialup. """
|
||||||
|
return bool (self.dialup_tags)
|
||||||
|
|
||||||
|
|
||||||
|
def is_tor_exit (self):
|
||||||
|
""" Test if this is a Tor exit node. """
|
||||||
|
return 'TOR' in self.blackhat_tags
|
||||||
|
|
||||||
|
|
||||||
|
def _hostnamesetter (self):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
the hostname attribute. """
|
||||||
|
def g (answer):
|
||||||
|
""" Store answer. """
|
||||||
|
self.hostname = answer[0]
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter (answers, tag_dict):
|
||||||
|
""" Lookup answers in tag_dict, return values of matches. """
|
||||||
|
return [ tag_dict[ip] for ip in answers if ip in tag_dict ]
|
||||||
|
|
||||||
|
|
||||||
|
def _tagsetter (self, dnsbl):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
our *tags attributes. """
|
||||||
|
def g (answer):
|
||||||
|
""" Store answer. """
|
||||||
|
self.blackhat_tags.update (self._filter (answer, dnsbl.blackhat_tags))
|
||||||
|
self.dialup_tags.update (self._filter (answer, dnsbl.dialup_tags))
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decode_txt (answer):
|
||||||
|
""" Helper: decode / unpack whois answer. """
|
||||||
|
try:
|
||||||
|
answer = answer[0][0].decode ('utf-8')
|
||||||
|
except UnicodeError:
|
||||||
|
answer = answer[0][0].decode ('iso-8859-1')
|
||||||
|
answer = answer.strip ('"').split ('|')
|
||||||
|
return [ a.strip () for a in answer if a ]
|
||||||
|
|
||||||
|
|
||||||
|
def _whoissetter_ss (self):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
the whois dict. """
|
||||||
|
def g (answer):
|
||||||
|
""" Store answer. """
|
||||||
|
self.whois = dict (zip (ShadowServer.fields, self._decode_txt (answer)))
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def _whoissetter_tc (self, aresolver):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
the right attribute. """
|
||||||
|
def g (answer):
|
||||||
|
""" Store answer. """
|
||||||
|
self.whois = dict (zip (TeamCymru.fields, self._decode_txt (answer)))
|
||||||
|
self.whois['org'] = None
|
||||||
|
# maybe there's still more info?
|
||||||
|
aresolver.query ('AS' + self.whois['asn'] + '.' + TeamCymru.asn_zone,
|
||||||
|
self._whoissetter_tc2 (), asyncdns.adns.rr.TXT)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def _whoissetter_tc2 (self):
|
||||||
|
""" Return a callable object that puts the answer into
|
||||||
|
the right attribute. """
|
||||||
|
def g (answer):
|
||||||
|
""" Store answer. """
|
||||||
|
self.whois['org'] = self._decode_txt (answer)[-1]
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# test IP 127.0.0.2 should give all positives
|
||||||
|
|
||||||
|
a = asyncdns.AsyncDNS (asyncdns.RESOLV_CONF)
|
||||||
|
i = IPInfo (a, sys.argv[1])
|
||||||
|
a.wait ()
|
||||||
|
a.cancel ()
|
||||||
|
|
||||||
|
print ('hostname: %s' % i.hostname)
|
||||||
|
for k in sorted (i.whois.keys ()):
|
||||||
|
print ("%s: %s" % (k, i.whois[k]))
|
||||||
|
for tag in sorted (i.tags):
|
||||||
|
print (tag)
|
||||||
|
if i.is_blackhat ():
|
||||||
|
print ('BLACKHAT')
|
||||||
|
if i.is_dialup ():
|
||||||
|
print ('DUL')
|
|
@ -0,0 +1,405 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
DON'T USE THIS PAGE FOR SCRAPING.
|
||||||
|
|
||||||
|
Seriously. You'll only get your IP blocked.
|
||||||
|
|
||||||
|
Read http://www.gutenberg.org/feeds/ to learn how to download Project
|
||||||
|
Gutenberg metadata much faster than by scraping.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:ebook="http://www.gutenberg.org/ebooks/"
|
||||||
|
xmlns:marcrel="http://www.loc.gov/loc.terms/relators/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
|
||||||
|
xml:lang="${os.lang}"
|
||||||
|
version="XHTML+RDFa 1.0">
|
||||||
|
|
||||||
|
<?python
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from libgutenberg.MediaTypes import mediatypes as mt
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
|
||||||
|
os.description = "Free kindle book and epub digitized and proofread by %s." % os.pg
|
||||||
|
|
||||||
|
old_header = os.entries[0].header # suppress first header as already in <h1>
|
||||||
|
def help_page (s = ''):
|
||||||
|
s = s.replace (' ', '_')
|
||||||
|
return '%s#%s' % ('/wiki/Gutenberg:Help_on_Bibliographic_Record_Page', s)
|
||||||
|
def explain (s):
|
||||||
|
return _('Explain {topic}.').format (topic = _(s))
|
||||||
|
def _ (s):
|
||||||
|
return s
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
maintainer = 'is-catalog-maintainer' in cherrypy.request.cookie
|
||||||
|
?>
|
||||||
|
|
||||||
|
<xi:include href="site-layout.html" />
|
||||||
|
<xi:include href="social-functions.html" />
|
||||||
|
|
||||||
|
<py:def function="help(topic)">
|
||||||
|
<a href="${help_page (topic)}"
|
||||||
|
title="${explain (topic)}"><span class="icon icon_help noprint" /></a>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<head profile="http://a9.com/-/spec/opensearch/1.1/"
|
||||||
|
xmlns:og="http://opengraphprotocol.org/schema/">
|
||||||
|
|
||||||
|
${site_head ()}
|
||||||
|
|
||||||
|
<title>${os.title} - Free Ebook</title>
|
||||||
|
|
||||||
|
<link rel="alternate nofollow" type="${mt.rdf}"
|
||||||
|
href="${os.url ('bibrec', id = os.id, format = 'rdf')}" />
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.qrcode { background: transparent url(${os.qrcode_url}) 0 0 no-repeat; }
|
||||||
|
</style>
|
||||||
|
<meta name="google" content="notranslate" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mw-head-dummy" class="noprint" />
|
||||||
|
|
||||||
|
<div id="content" itemscope="itemscope" itemtype="http://schema.org/Book" i18n:comment="On the 'bibrec' page.">
|
||||||
|
|
||||||
|
<div class="breadcrumbs noprint">
|
||||||
|
<ul>
|
||||||
|
<py:for each="n, bc in enumerate (os.breadcrumbs)">
|
||||||
|
<li class="breadcrumb ${'first' if n == 0 else 'next'}"
|
||||||
|
itemscope="itemscope" itemtype="http://data-vocabulary.org/Breadcrumb">
|
||||||
|
<span class="breadcrumb-separator"></span>
|
||||||
|
<a href="${bc[2]}" title="${bc[1]}" itemprop="url"><span itemprop="title">${bc[0]}</span></a>
|
||||||
|
</li>
|
||||||
|
</py:for>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1 itemprop="name">${os.title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div property="dcterms:publisher" itemprop="publisher" content="Project Gutenberg" />
|
||||||
|
<div itemprop="bookFormat" content="EBook" />
|
||||||
|
|
||||||
|
<div id="cover-social-wrapper">
|
||||||
|
|
||||||
|
<div py:if="os.cover_image_url" id="cover">
|
||||||
|
<img class="cover-art"
|
||||||
|
src="${os.cover_image_url}"
|
||||||
|
title="Book Cover"
|
||||||
|
alt="Book Cover"
|
||||||
|
itemprop="image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div py:if="not os.cover_image_url" id="no-cover">
|
||||||
|
<div class="icon icon_${os.title_icon}" />
|
||||||
|
<div class="text">No cover available</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="social" class="noprint">
|
||||||
|
<ul>
|
||||||
|
<!--! Broken. Dialog opens and closes immediately. Disabled for now.
|
||||||
|
<li>
|
||||||
|
${fb_share (os.canonical_url, os.title.encode ('utf-8'),
|
||||||
|
os.description.encode ('utf-8'), os.cover_thumb_url)}
|
||||||
|
</li>
|
||||||
|
-->
|
||||||
|
<li>
|
||||||
|
${gplus_share (os.canonical_url)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
${tw_share (os.canonical_url, os.twit)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a onclick="printpage ()" title="Print this page"><span class="icon icon_print" /></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="qr">
|
||||||
|
<!--! qr code of desktop page for print -->
|
||||||
|
<span class="qrcode qrcode-desktop noscreen" />
|
||||||
|
<!--! qr code of mobile page for screen -->
|
||||||
|
<span class="qrcode qrcode-mobile noprint" title="Go to our mobile site." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabs-wrapper">
|
||||||
|
<div id="tabs">
|
||||||
|
<ul class="noprint">
|
||||||
|
<li><a href="#download">Download</a></li>
|
||||||
|
<li><a href="#bibrec">Bibrec</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div id="bibrec" i18n:comment="On the 'bibrec' tab of the 'bibrec' page.">
|
||||||
|
<py:for each="e in os.entries">
|
||||||
|
<py:if test="isinstance (e, bs.DC)">
|
||||||
|
|
||||||
|
<div typeof="pgterms:ebook" about="[ebook:$e.project_gutenberg_id]">
|
||||||
|
|
||||||
|
<h2>Bibliographic Record <span>${help (_('Table: Bibliographic Record'))}</span></h2>
|
||||||
|
|
||||||
|
<table class="bibrec" summary="Bibliographic data of author and book.">
|
||||||
|
<colgroup>
|
||||||
|
<col class="narrow" />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
|
<tr py:for="author in e.authors">
|
||||||
|
<th>${author.role}</th>
|
||||||
|
<td>
|
||||||
|
<a href="${os.url ('author', id = author.id)}"
|
||||||
|
rel="marcrel:${author.marcrel}" about="/authors/${author.id}" typeof="pgterms:agent"
|
||||||
|
itemprop="creator">${author.name_and_dates}</a></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="marc in e.marcs">
|
||||||
|
<th>${marc.caption}</th>
|
||||||
|
<py:choose test="">
|
||||||
|
<td py:when="marc.code == '010'">
|
||||||
|
<a class="external"
|
||||||
|
href="http://lccn.loc.gov/${marc.text}"
|
||||||
|
title="Look up this book in the Library of Congress catalog.">${marc.text} <span class="icon icon_external_link"/></a>
|
||||||
|
</td>
|
||||||
|
<td py:when="marc.code[0]=='5'">
|
||||||
|
<?python
|
||||||
|
text = gg.xmlspecialchars (marc.text)
|
||||||
|
text = re.sub (r'(//\S+)', r'<a href="\1">\1</a>', text)
|
||||||
|
text = re.sub (r'#(\d+)', r'<a href="/ebooks/\1">#\1</a>', text)
|
||||||
|
?>
|
||||||
|
${ Markup (gg.insert_breaks (text)) }
|
||||||
|
</td>
|
||||||
|
<td py:when="marc.code=='245'" itemprop="headline">
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</td>
|
||||||
|
<td py:when="marc.code=='240'" itemprop="alternativeHeadline">
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</td>
|
||||||
|
<td py:when="marc.code=='246'" itemprop="alternativeHeadline">
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</td>
|
||||||
|
<td py:otherwise="">
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</td>
|
||||||
|
</py:choose>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="language in e.languages"
|
||||||
|
property="dcterms:language" datatype="dcterms:RFC4646" itemprop="inLanguage" content="${language.id}">
|
||||||
|
<th>Language</th>
|
||||||
|
<td py:if="language.id != 'en'"><a href="/browse/languages/${language.id}">${language.language}</a></td>
|
||||||
|
<td py:if="language.id == 'en'">${language.language}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="locc in e.loccs"
|
||||||
|
property="dcterms:subject" datatype="dcterms:LCC" content="${locc.id}">
|
||||||
|
<th>LoC Class</th>
|
||||||
|
<td>
|
||||||
|
<a href="/browse/loccs/${locc.id.lower ()}">${locc.id}: ${locc.locc}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="subject in e.subjects">
|
||||||
|
<th>Subject</th>
|
||||||
|
<td property="dcterms:subject" datatype="dcterms:LCSH">
|
||||||
|
<a class="block" href="${os.url ('subject', id = subject.id)}">
|
||||||
|
${subject.subject}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="dcmitype in e.dcmitypes">
|
||||||
|
<th>Category</th>
|
||||||
|
<td property="dcterms:type" datatype="dcterms:DCMIType">${dcmitype.description}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>EBook-No.</th>
|
||||||
|
<td>${e.project_gutenberg_id}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr property="dcterms:issued" datatype="xsd:date" content="${e.xsd_release_date_time}">
|
||||||
|
<th>Release Date</th>
|
||||||
|
<td itemprop="datePublished">${e.hr_release_date}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Copyright Status</th>
|
||||||
|
<td property="dcterms:rights">${e.rights}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Downloads</th>
|
||||||
|
<td itemprop="interactionCount" i18n:msg="count">${e.downloads} downloads in the last 30 days.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer">
|
||||||
|
<th>Price</th>
|
||||||
|
<td><span itemprop="priceCurrency" content="USD" /><span itemprop="price">$0.00</span><span itemprop="availability" href="http://schema.org/InStock" content="In Stock" /></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<div id="more_stuff">
|
||||||
|
<py:for each="n, e in enumerate (os.entries)">
|
||||||
|
<py:if test="isinstance (e, bs.Cat) and e.rel not in ('start', )">
|
||||||
|
<py:if test="e.header and old_header != e.header">
|
||||||
|
<h2 class="header">${e.header}</h2>
|
||||||
|
</py:if>
|
||||||
|
<?python old_header = e.header ?>
|
||||||
|
|
||||||
|
<div class="${e.class_}">
|
||||||
|
<a rel="nofollow"
|
||||||
|
href="${e.url}"
|
||||||
|
type="${e.type}"
|
||||||
|
charset="${e.charset}"
|
||||||
|
accesskey="${str (n % 10)}">
|
||||||
|
<span class="cell leftcell">
|
||||||
|
<span class="icon icon_${e.icon}" />
|
||||||
|
</span>
|
||||||
|
<span class="cell content">
|
||||||
|
<span class="title">${e.title}</span>
|
||||||
|
<span py:if="e.subtitle" class="subtitle">${e.subtitle}</span>
|
||||||
|
<span py:if="e.extra" class="extra">${e.extra}</span>
|
||||||
|
</span>
|
||||||
|
<span class="hstrut" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
</div> <!-- more stuff -->
|
||||||
|
|
||||||
|
</div> <!--! bibrec -->
|
||||||
|
|
||||||
|
<div id="download" i18n:comment="On the 'Download' tab of the 'bibrec' page.">
|
||||||
|
|
||||||
|
<py:for each="e in os.entries">
|
||||||
|
<py:if test="isinstance (e, bs.DC)">
|
||||||
|
<div about="[ebook:$e.project_gutenberg_id]" rel="dcterms:hasFormat" rev="dcterms:isFormatOf">
|
||||||
|
|
||||||
|
<h2>Download This eBook</h2>
|
||||||
|
|
||||||
|
<table class="files" summary="Table of available file types and sizes.">
|
||||||
|
<colgroup>
|
||||||
|
<col class="narrow" />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col class="narrow noprint" />
|
||||||
|
<col class="narrow noprint" />
|
||||||
|
<col class="narrow noprint" />
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>Format <span>${help ('Format')}</span></th>
|
||||||
|
<th class="noscreen">Url</th>
|
||||||
|
<th i18n:comment="Size of a file." class="right">Size</th>
|
||||||
|
<th class="noprint"><span>${help ('Dropbox')}</span></th>
|
||||||
|
<th class="noprint"><span>${help ('Google Drive')}</span></th>
|
||||||
|
<th class="noprint"><span>${help ('OneDrive')}</span></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr py:for="i, file_ in enumerate (e.files)"
|
||||||
|
py:if="not file_.hidden"
|
||||||
|
class="${i%2 and 'odd' or 'even'}"
|
||||||
|
about="${file_.url}" typeof="pgterms:file">
|
||||||
|
<td><span class="icon icon_${e.icon}" /></td>
|
||||||
|
<td property="dcterms:format" content="${file_.mediatypes[-1]}" datatype="dcterms:IMT"
|
||||||
|
class="unpadded icon_save"
|
||||||
|
><a href="${file_.url}" type="${file_.mediatypes[-1]}" charset="${file_.encoding}"
|
||||||
|
class="link"
|
||||||
|
title="Download">${file_.hr_filetype}</a></td>
|
||||||
|
<td class="noscreen">${file_.url}</td>
|
||||||
|
<td class="right"
|
||||||
|
property="dcterms:extent" content="${file_.extent}">${file_.hr_extent}</td>
|
||||||
|
<td class="noprint">
|
||||||
|
<a py:if="file_.dropbox_url"
|
||||||
|
href="${file_.dropbox_url}"
|
||||||
|
title="Send to Dropbox." rel="nofollow"><span class="icon icon_dropbox" /></a>
|
||||||
|
</td>
|
||||||
|
<td class="noprint">
|
||||||
|
<a py:if="file_.gdrive_url"
|
||||||
|
href="${file_.gdrive_url}"
|
||||||
|
title="Send to Google Drive." rel="nofollow"><span class="icon icon_gdrive" /></a>
|
||||||
|
</td>
|
||||||
|
<td class="noprint">
|
||||||
|
<a py:if="file_.honeypot_url"
|
||||||
|
href="${file_.honeypot_url}"
|
||||||
|
title="Send to MegaUpload." rel="nofollow" />
|
||||||
|
<a py:if="file_.msdrive_url"
|
||||||
|
href="${file_.msdrive_url}"
|
||||||
|
title="Send to OneDrive." rel="nofollow"><span class="icon icon_msdrive" /></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!--! more files ... -->
|
||||||
|
<tr py:if="e.base_dir"
|
||||||
|
class="${i%2 and 'odd' or 'even'}">
|
||||||
|
<td><span class="icon icon_folder" /></td>
|
||||||
|
<td class="unpadded icon_file"><a href="${e.base_dir}" class="link"
|
||||||
|
i18n:comment="Link to the directory containing all files.">More Files…</a></td>
|
||||||
|
<td class="noscreen">http:${os.qualify (e.base_dir)}</td>
|
||||||
|
<td/>
|
||||||
|
<td class="noprint"><!--! dropbox column --></td>
|
||||||
|
<td class="noprint"><!--! gdrive column --></td>
|
||||||
|
<td class="noprint"><!--! msdrive column --></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
</div> <!-- download -->
|
||||||
|
|
||||||
|
</div> <!--! tabs -->
|
||||||
|
</div> <!--! tabs-wrapper -->
|
||||||
|
</div> <!--! body -->
|
||||||
|
|
||||||
|
<div id="dialog" class="hidden">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--!
|
||||||
|
<a href="http://validator.w3.org/check?uri=referer" rel="nofollow"
|
||||||
|
title="This page contains RDFa metadata."><img
|
||||||
|
src="http://www.w3.org/Icons/valid-xhtml-rdfa-blue"
|
||||||
|
width="88"
|
||||||
|
height="31"
|
||||||
|
alt="Valid XHTML + RDFa"
|
||||||
|
about=""
|
||||||
|
rel="dcterms:conformsTo"
|
||||||
|
resource="http://www.w3.org/TR/rdfa-syntax" /></a>
|
||||||
|
|
||||||
|
<a href="${os.url_carry ('bibrec', format='rdf')}"
|
||||||
|
title="Download RDF/XML Metadata for this ebook."><img
|
||||||
|
src="http://www.w3.org/RDF/icons/rdf_metadata_button.32"
|
||||||
|
width="76"
|
||||||
|
height="32"
|
||||||
|
alt="RDF/XML Metadata" /></a>
|
||||||
|
-->
|
||||||
|
|
||||||
|
${site_footer ()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_top ()}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
i18n:comment="The help menu for the search box.">
|
||||||
|
|
||||||
|
<p>Enter your search terms separated by spaces,
|
||||||
|
then press <Enter>.
|
||||||
|
Avoid punctuation except as indicated below:</p>
|
||||||
|
|
||||||
|
<table class="helpbox">
|
||||||
|
<tr>
|
||||||
|
<th>Suffixes</th>
|
||||||
|
<th xml:lang="en">.</th>
|
||||||
|
<td>exact match</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="7">Prefixes</th>
|
||||||
|
<th xml:lang="en">a.</th>
|
||||||
|
<td>author</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">t.</th>
|
||||||
|
<td>title</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">s.</th>
|
||||||
|
<td>subject</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">l.</th>
|
||||||
|
<td>language</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">#</th>
|
||||||
|
<td>ebook no.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">n.</th>
|
||||||
|
<td>ebook no.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">cat.</th>
|
||||||
|
<td>category</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="3" style="width: 8em">
|
||||||
|
Operators
|
||||||
|
<small>Always put spaces around these.</small>
|
||||||
|
</th>
|
||||||
|
<th xml:lang="en">|</th>
|
||||||
|
<td>or</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">!</th>
|
||||||
|
<td>not</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th xml:lang="en">( )</th>
|
||||||
|
<td>grouping</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="helpbox">
|
||||||
|
<tr>
|
||||||
|
<th>this query</th>
|
||||||
|
<th>finds</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">shakespeare hamlet</td>
|
||||||
|
<td>"Hamlet" by Shakespeare</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">qui.</td>
|
||||||
|
<td>"qui", not "Quixote"</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">love stories</td>
|
||||||
|
<td>love stories</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">a.shakespeare</td>
|
||||||
|
<td>by Shakespeare</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">s.shakespeare</td>
|
||||||
|
<td>about Shakespeare</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">#74</td>
|
||||||
|
<td>ebook no. 74</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">juvenile l.german</td>
|
||||||
|
<td>juvenile lit in German</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">verne ( l.fr | l.it )</td>
|
||||||
|
<td>by Verne in French or Italian</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">love stories ! austen</td>
|
||||||
|
<td>love stories not by Austen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td xml:lang="en">jane austen cat.audio</td>
|
||||||
|
<td>audio books by Jane Austen</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n">
|
||||||
|
|
||||||
|
<xi:include href="site-layout.html" />
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
${site_head ()}
|
||||||
|
|
||||||
|
<title>Captcha</title>
|
||||||
|
<style type="text/css">
|
||||||
|
.ui-dialog-titlebar-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#recaptcha_response_field {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.recaptcha_only_if_incorrect_sol {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript" src="//www.google.com/recaptcha/api/js/recaptcha_ajax.js" />
|
||||||
|
<script type="text/javascript">
|
||||||
|
function on_recaptcha_loaded () {
|
||||||
|
$('#need_javascript').hide ();
|
||||||
|
Recaptcha.focus_response_field ();
|
||||||
|
}
|
||||||
|
var RecaptchaOptions = {
|
||||||
|
theme: "custom",
|
||||||
|
custom_theme_widget: "recaptcha_widget",
|
||||||
|
callback: on_recaptcha_loaded
|
||||||
|
};
|
||||||
|
|
||||||
|
require (
|
||||||
|
[
|
||||||
|
'jquery',
|
||||||
|
'jquery-ui/dialog',
|
||||||
|
'jquery.cookie/jquery.cookie'
|
||||||
|
],
|
||||||
|
function (jquery, dialog, cookie) {
|
||||||
|
if (jquery.cookie ('session_id')) {
|
||||||
|
jquery ('#need_cookies').hide ();
|
||||||
|
}
|
||||||
|
Recaptcha.create ("${os.recaptcha_public_key}", "id_captcha", RecaptchaOptions);
|
||||||
|
var dlg = jquery ('#dialog');
|
||||||
|
dlg.dialog ({
|
||||||
|
width: 350,
|
||||||
|
resizable: false,
|
||||||
|
modal: true,
|
||||||
|
closeOnEscape: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mw-head-dummy" class="noprint" />
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
|
||||||
|
<div id="dialog" title="Are you human?" class="hidden">
|
||||||
|
|
||||||
|
<p py:if="os.error is not None" style="color: red">Incorrect please try again</p>
|
||||||
|
|
||||||
|
<p>You have used Project Gutenberg quite a lot today or clicked through it really fast. Email webmaster2017@pglaf.org to report problems. To make sure you are human, we ask you to resolve this captcha:</p>
|
||||||
|
|
||||||
|
<form method="post" action="/w/captcha/answer/">
|
||||||
|
<div id="recaptcha_widget">
|
||||||
|
<div id="recaptcha_image"></div>
|
||||||
|
<p class="recaptcha_only_if_incorrect_sol">Incorrect please try again</p>
|
||||||
|
<p class="recaptcha_only_if_image">Enter the words you see:</p>
|
||||||
|
<p class="recaptcha_only_if_audio">Enter the numbers you hear:</p>
|
||||||
|
<input type="text" id="recaptcha_response_field" name="recaptcha_response_field" />
|
||||||
|
<input type="submit" name="SubmitButton" value="Submit" />
|
||||||
|
<input type="button" name="ReloadButton" value="Get another captcha"
|
||||||
|
onclick="Recaptcha.reload ()" />
|
||||||
|
<input type="button" name="AudioButton" value="Get an audio captcha"
|
||||||
|
class="recaptcha_only_if_image" onclick="Recaptcha.switch_type ('audio')" />
|
||||||
|
<input type="button" name="ImageButton" value="Get an image captcha"
|
||||||
|
class="recaptcha_only_if_audio" onclick="Recaptcha.switch_type ('image')" />
|
||||||
|
<input type="button" name="HelpButton" value="Help"
|
||||||
|
onclick="Recaptcha.showhelp ()" />
|
||||||
|
<p id="need_cookies">Project Gutenberg works better with cookies enabled.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="need_javascript">You need javascript for this.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_footer ()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_top ()}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--! recaptcha is broken with application/xhtml+xml -->
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xml:lang="${os.lang}">
|
||||||
|
|
||||||
|
<xi:include href="site-layout.mobile" />
|
||||||
|
|
||||||
|
<head>
|
||||||
|
${site_head ()}
|
||||||
|
|
||||||
|
<title>Captcha</title>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
#recaptcha_response_field {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
.recaptcha_only_if_incorrect_sol {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="//www.google.com/recaptcha/api/js/recaptcha_ajax.js" />
|
||||||
|
<script type="application/javascript">
|
||||||
|
function on_recaptcha_loaded () {
|
||||||
|
$('#need_javascript').hide ();
|
||||||
|
Recaptcha.focus_response_field ();
|
||||||
|
}
|
||||||
|
var RecaptchaOptions = {
|
||||||
|
theme: "custom",
|
||||||
|
custom_theme_widget: "recaptcha_widget",
|
||||||
|
callback: on_recaptcha_loaded
|
||||||
|
};
|
||||||
|
|
||||||
|
require (
|
||||||
|
[
|
||||||
|
'jquery',
|
||||||
|
'jquery.cookie/jquery.cookie'
|
||||||
|
],
|
||||||
|
function (jquery, cookie) {
|
||||||
|
if (jquery.cookie ('session_id')) {
|
||||||
|
jquery ('#need_cookies').hide ();
|
||||||
|
}
|
||||||
|
Recaptcha.create ("${os.recaptcha_public_key}", "id_captcha", RecaptchaOptions);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<p>You have used Project Gutenberg quite a lot today or clicked through it really fast. To make sure you are human, we ask you to resolve this captcha:</p>
|
||||||
|
|
||||||
|
<form method="post" action="/w/captcha/answer/">
|
||||||
|
<div id="recaptcha_widget">
|
||||||
|
<div id="recaptcha_image"></div>
|
||||||
|
<p class="recaptcha_only_if_incorrect_sol">Incorrect please try again</p>
|
||||||
|
<p class="recaptcha_only_if_image">Enter the words you see:</p>
|
||||||
|
<p class="recaptcha_only_if_audio">Enter the numbers you hear:</p>
|
||||||
|
<input type="text" id="recaptcha_response_field" name="recaptcha_response_field" />
|
||||||
|
<input type="submit" name="SubmitButton" value="Submit" />
|
||||||
|
<input type="button" name="ReloadButton" value="Get another captcha"
|
||||||
|
onclick="Recaptcha.reload ()" />
|
||||||
|
<input type="button" name="AudioButton" value="Get an audio captcha"
|
||||||
|
class="recaptcha_only_if_image" onclick="Recaptcha.switch_type ('audio')" />
|
||||||
|
<input type="button" name="ImageButton" value="Get an image captcha"
|
||||||
|
class="recaptcha_only_if_audio" onclick="Recaptcha.switch_type ('image')" />
|
||||||
|
<input type="button" name="HelpButton" value="Help"
|
||||||
|
onclick="Recaptcha.showhelp ()" />
|
||||||
|
<p id="need_cookies">Project Gutenberg works better with cookies enabled.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p id="need_javascript">You need javascript for this.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_footer ()}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,142 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
DON'T USE THIS PAGE FOR SCRAPING.
|
||||||
|
|
||||||
|
Seriously. You'll only get your IP blocked.
|
||||||
|
|
||||||
|
Download http://www.gutenberg.org/feeds/catalog.rdf.bz2 instead,
|
||||||
|
which contains *all* Project Gutenberg metadata in one RDF/XML file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:ebook="http://www.gutenberg.org/ebooks/"
|
||||||
|
xmlns:marcrel="http://www.loc.gov/loc.terms/relators/"
|
||||||
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
|
||||||
|
xmlns:og="http://opengraphprotocol.org/schema/"
|
||||||
|
xmlns:fb="http://www.facebook.com/2008/fbml"
|
||||||
|
xml:lang="${os.lang}"
|
||||||
|
version="XHTML+RDFa 1.0">
|
||||||
|
|
||||||
|
<?python
|
||||||
|
old_header = os.title
|
||||||
|
os.status_line = ''
|
||||||
|
?>
|
||||||
|
|
||||||
|
<xi:include href="site-layout.html" />
|
||||||
|
<xi:include href="social-functions.html" />
|
||||||
|
|
||||||
|
<head profile="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
${site_head ()}
|
||||||
|
|
||||||
|
<title>${os.title} - ${os.pg}</title>
|
||||||
|
|
||||||
|
<link rel="self"
|
||||||
|
i18n:comment="Link pointing to the same page the user is viewing."
|
||||||
|
title="This Page"
|
||||||
|
href="${os.url ('search', start_index = os.start_index)}" />
|
||||||
|
|
||||||
|
<link py:if="os.show_prev_page_link"
|
||||||
|
rel="first"
|
||||||
|
title="First Page"
|
||||||
|
href="${os.url ('search')}" />
|
||||||
|
|
||||||
|
<link py:if="os.show_prev_page_link"
|
||||||
|
rel="previous"
|
||||||
|
title="Previous Page"
|
||||||
|
href="${os.url ('search', start_index = os.prev_page_index)}" />
|
||||||
|
|
||||||
|
<link py:if="os.show_next_page_link"
|
||||||
|
rel="next"
|
||||||
|
title="Next Page"
|
||||||
|
href="${os.url ('search', start_index = os.next_page_index)}" />
|
||||||
|
|
||||||
|
<meta name="totalResults" content="${os.total_results}" />
|
||||||
|
<meta name="startIndex" content="${os.start_index}" />
|
||||||
|
<meta name="itemsPerPage" content="${os.items_per_page}" />
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="mw-head-dummy" class="noprint" />
|
||||||
|
|
||||||
|
<div id="content" i18n:comment="On the page of results of a search.">
|
||||||
|
<div class="header">
|
||||||
|
<h1><span class="icon icon_${os.title_icon}" />${os.title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div>
|
||||||
|
<ul class="results">
|
||||||
|
<py:for each="n, e in enumerate (os.entries)">
|
||||||
|
|
||||||
|
<py:if test="e.header and old_header != e.header">
|
||||||
|
<li class="h2">
|
||||||
|
<h2 class="padded">${e.header}</h2>
|
||||||
|
</li>
|
||||||
|
<?python old_header = e.header ?>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<py:if test="isinstance (e, bs.Cat) and e.rel not in ('start', )">
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="e.rel == '__statusline__'">
|
||||||
|
<li class="statusline">
|
||||||
|
<div class="padded">
|
||||||
|
<span>${e.title}</span> ${prev_next_links ()}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?python os.status_line = e.title ?>
|
||||||
|
</py:when>
|
||||||
|
|
||||||
|
<py:otherwise>
|
||||||
|
<li class="${e.class_}">
|
||||||
|
<a class="link" href="${e.url}" accesskey="${str (n % 10)}">
|
||||||
|
<span class="cell leftcell${' with-cover' if e.thumb_url else ' without-cover'}">
|
||||||
|
<span py:if="not e.thumb_url" class="icon-wrapper">
|
||||||
|
<span class="icon icon_${e.icon}" />
|
||||||
|
</span>
|
||||||
|
<img py:if="e.thumb_url"
|
||||||
|
class="cover-thumb" src="${e.thumb_url}" alt="" />
|
||||||
|
</span>
|
||||||
|
<span class="cell content">
|
||||||
|
<span py:if="e.title" class="title">${e.title}</span>
|
||||||
|
<span py:if="e.subtitle" class="subtitle">${e.subtitle}</span>
|
||||||
|
<span py:if="e.extra" class="extra">${e.extra}</span>
|
||||||
|
</span>
|
||||||
|
<span class="hstrut" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</py:otherwise>
|
||||||
|
|
||||||
|
</py:choose>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<py:if test="os.status_line != ''">
|
||||||
|
<li class="statusline">
|
||||||
|
<div class="padded">
|
||||||
|
${os.status_line} ${prev_next_links ()}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!--! body -->
|
||||||
|
|
||||||
|
${site_footer ()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_top ()}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,151 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
DON'T USE THIS PAGE FOR SCRAPING.
|
||||||
|
|
||||||
|
Seriously. You'll only get your IP blocked.
|
||||||
|
|
||||||
|
Download http://www.gutenberg.org/feeds/catalog.rdf.bz2 instead,
|
||||||
|
which contains *all* Project Gutenberg metadata in one RDF/XML file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xml:lang="${os.lang}">
|
||||||
|
|
||||||
|
<?python
|
||||||
|
import re
|
||||||
|
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
old_header = ''
|
||||||
|
|
||||||
|
def help_page (s = ''):
|
||||||
|
s = s.replace (' ', '_')
|
||||||
|
return '%s#%s' % ('/wiki/Gutenberg:Help_on_Bibliographic_Record_Page', s)
|
||||||
|
?>
|
||||||
|
|
||||||
|
<xi:include href="site-layout.mobile" />
|
||||||
|
|
||||||
|
<head profile="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
${site_head ()}
|
||||||
|
|
||||||
|
<title>${os.title}</title>
|
||||||
|
|
||||||
|
<meta name="totalResults" content="${os.total_results}" />
|
||||||
|
<meta name="startIndex" content="${os.start_index}" />
|
||||||
|
<meta name="itemsPerPage" content="${os.items_per_page}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="content" i18n:comment="On the page of results of a search.">
|
||||||
|
<ol class="results">
|
||||||
|
|
||||||
|
${search_box ()}
|
||||||
|
|
||||||
|
<py:for each="n, e in enumerate (os.entries)">
|
||||||
|
<py:if test="e.header and old_header != e.header">
|
||||||
|
<li class="header">${e.header}</li>
|
||||||
|
</py:if>
|
||||||
|
<?python old_header = e.header ?>
|
||||||
|
|
||||||
|
<py:if test="isinstance (e, bs.DC)">
|
||||||
|
<li class="bibrec">
|
||||||
|
<div class="table">
|
||||||
|
<div class="row">
|
||||||
|
<div class="cell leftcell">
|
||||||
|
<div class="icon icon_${e.icon}" />
|
||||||
|
</div>
|
||||||
|
<div class="cell content">
|
||||||
|
<!--! get coverpage first, it floats to the right -->
|
||||||
|
<img py:if="e.cover_image" class="coverpage"
|
||||||
|
src="${e.cover_image.url}" alt="[Coverpage]" />
|
||||||
|
<p py:for="author in e.authors">${author.role}: ${author.name_and_dates}</p>
|
||||||
|
<py:for each="marc in e.marcs">
|
||||||
|
<py:choose test="">
|
||||||
|
<p py:when="marc.code[0]=='5'">
|
||||||
|
<?python
|
||||||
|
text = gg.xmlspecialchars (marc.text)
|
||||||
|
text = re.sub (r'(//\S+)', r'<a href="\1">\1</a>', text)
|
||||||
|
text = re.sub (r'#(\d+)',
|
||||||
|
r'<a href="/ebooks/\1.mobile">#\1</a>', text)
|
||||||
|
?>
|
||||||
|
${marc.caption}:
|
||||||
|
${ Markup (gg.insert_breaks (text)) }
|
||||||
|
</p>
|
||||||
|
<p py:otherwise="">
|
||||||
|
${marc.caption}:
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</p>
|
||||||
|
</py:choose>
|
||||||
|
</py:for>
|
||||||
|
<p>Ebook No.: ${e.project_gutenberg_id}</p>
|
||||||
|
<p>Published: ${e.hr_release_date}</p>
|
||||||
|
<p>Downloads: ${e.downloads}</p>
|
||||||
|
<p py:for="language in e.languages">Language: ${language.language}</p>
|
||||||
|
<p py:for="subject in e.subjects">Subject: ${subject.subject}</p>
|
||||||
|
<p py:for="locc in e.loccs">LoCC: ${locc.locc}</p>
|
||||||
|
<p py:for="category in e.categories">Category: ${category}</p>
|
||||||
|
<p>Rights: ${e.rights}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<py:if test="isinstance (e, bs.Cat)">
|
||||||
|
<py:choose test="">
|
||||||
|
<py:when test="e.rel == '__statusline__'" />
|
||||||
|
|
||||||
|
<py:otherwise>
|
||||||
|
<li class="${e.class_}">
|
||||||
|
<a class="table link" href="${e.url}" accesskey="${str (n % 10)}">
|
||||||
|
<span class="row">
|
||||||
|
<span class="cell leftcell">
|
||||||
|
<span class="icon icon_${e.icon}" />
|
||||||
|
</span>
|
||||||
|
<span class="cell content">
|
||||||
|
<span py:if="e.title" class="title">${e.title}</span>
|
||||||
|
<span py:if="e.subtitle" class="subtitle">${e.subtitle}</span>
|
||||||
|
<span py:if="e.extra" class="extra">${e.extra}</span>
|
||||||
|
</span>
|
||||||
|
<span py:if="e.icon2" class="cell rightcell">
|
||||||
|
<span class="icon icon_${e.icon2}" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<py:if test="os.show_next_page_link">
|
||||||
|
<li class="navlink more grayed">
|
||||||
|
<a class="table link"
|
||||||
|
href="${os.url_carry (start_index = os.next_page_index)}">
|
||||||
|
<span class="row">
|
||||||
|
<span class="cell leftcell">
|
||||||
|
<span class="icon icon_more" />
|
||||||
|
</span>
|
||||||
|
<span class="cell content">
|
||||||
|
<span class="title"><span>Next Page</span>…</span>
|
||||||
|
</span>
|
||||||
|
<span class="cell rightcell">
|
||||||
|
<span class="icon spinner" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</py:if>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${site_footer ()}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,293 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
DON'T USE THIS PAGE FOR SCRAPING.
|
||||||
|
|
||||||
|
Seriously. You'll only get your IP blocked.
|
||||||
|
|
||||||
|
Download http://www.gutenberg.org/feeds/catalog.rdf.bz2 instead,
|
||||||
|
which contains *all* Project Gutenberg metadata in one RDF/XML file.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<?python
|
||||||
|
import re
|
||||||
|
from libgutenberg import GutenbergGlobals as gg
|
||||||
|
|
||||||
|
if os.format == 'stanza':
|
||||||
|
os.type_opds = "application/atom+xml"
|
||||||
|
opds_relations = {
|
||||||
|
'cover': 'x-stanza-cover-image',
|
||||||
|
'thumb': 'x-stanza-cover-image-thumbnail',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
opds_relations = {
|
||||||
|
'new': 'http://opds-spec.org/sort/new',
|
||||||
|
'popular': 'http://opds-spec.org/sort/popular',
|
||||||
|
'cover': 'http://opds-spec.org/image',
|
||||||
|
'thumb': 'http://opds-spec.org/image/thumbnail',
|
||||||
|
'acquisition': 'http://opds-spec.org/acquisition',
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:opds="http://opds-spec.org/2010/catalog"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
|
||||||
|
xmlns:relevance="http://a9.com/-/opensearch/extensions/relevance/1.0/"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
|
||||||
|
<id>${os.url_carry (host = os.host, start_index = os.start_index)}</id>
|
||||||
|
|
||||||
|
<updated>${os.now}</updated>
|
||||||
|
<title>${os.title}</title>
|
||||||
|
<subtitle>Free ebooks since 1971.</subtitle>
|
||||||
|
<author>
|
||||||
|
<name>Marcello Perathoner</name>
|
||||||
|
<uri>http://www.gutenberg.org</uri>
|
||||||
|
<email>webmaster@gutenberg.org</email>
|
||||||
|
</author>
|
||||||
|
<icon>${os.qualify ('/pics/favicon.png')}</icon>
|
||||||
|
|
||||||
|
<py:choose test="os.opensearch_support">
|
||||||
|
<py:when test="2">
|
||||||
|
<!--! fake opensearch support in Stanza and Aldiko -->
|
||||||
|
|
||||||
|
<!--! The next 2 links are for Stanza that can't read the standard opensearch description.
|
||||||
|
Stanza even requires unescaped '{' and '}' which are not valid characters in urls! AARGH!!
|
||||||
|
Aldiko needs fully qualified urls here!
|
||||||
|
-->
|
||||||
|
<link rel="search"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
title="${os.placeholder}"
|
||||||
|
href="${os.add_amp (os.url ('search', host = os.host))}query={searchTerms}" />
|
||||||
|
<!--! routes would quote the invalid '{' and '}' -->
|
||||||
|
|
||||||
|
<link rel="x-stanza-search-suggestions"
|
||||||
|
type="application/x-suggestions+json"
|
||||||
|
href="${os.add_amp (os.url ('suggest', host = os.host, format = None))}query={searchTerms}" />
|
||||||
|
<!--! routes would quote '{' and '}' -->
|
||||||
|
</py:when>
|
||||||
|
|
||||||
|
<py:when test="1">
|
||||||
|
<!--! real opensearch support -->
|
||||||
|
|
||||||
|
<link rel="search"
|
||||||
|
type="application/opensearchdescription+xml"
|
||||||
|
title="Project Gutenberg Catalog Search"
|
||||||
|
href="${os.osd_url}" />
|
||||||
|
</py:when>
|
||||||
|
</py:choose>
|
||||||
|
|
||||||
|
<link rel="self"
|
||||||
|
title="This Page"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
href="${os.url_carry (start_index = os.start_index)}" />
|
||||||
|
|
||||||
|
<link rel="alternate"
|
||||||
|
type="text/html"
|
||||||
|
title="HTML Page"
|
||||||
|
href="${os.url_carry (format = 'html', start_index = os.start_index)}" />
|
||||||
|
|
||||||
|
<link rel="start"
|
||||||
|
title="Start Page"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
href="${os.url ('start')}" />
|
||||||
|
|
||||||
|
<link py:if="os.show_prev_page_link"
|
||||||
|
rel="first"
|
||||||
|
title="First Page"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
href="${os.url_carry (start_index = 1)}" />
|
||||||
|
|
||||||
|
<link py:if="os.show_prev_page_link"
|
||||||
|
rel="previous"
|
||||||
|
title="Previous Page"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
href="${os.url_carry (start_index = os.prev_page_index)}" />
|
||||||
|
|
||||||
|
<!--! Coolreader sucks up to 1000 entries without user paging. See:
|
||||||
|
http://crengine.git.sourceforge.net/git/gitweb.cgi?p=crengine/crengine;a=blob;f=android/src/org/coolreader/crengine/OPDSUtil.java
|
||||||
|
We give it one page. That's enough.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<link py:if="os.show_next_page_link and not os.user_agent.startswith ('CoolReader/')"
|
||||||
|
rel="next"
|
||||||
|
title="Next Page"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
href="${os.url_carry (start_index = os.next_page_index)}" />
|
||||||
|
|
||||||
|
<py:for each="e in os.entries">
|
||||||
|
<py:if test="isinstance (e, bs.Cat) and e.rel in opds_relations">
|
||||||
|
<link rel="${opds_relations[e.rel]}"
|
||||||
|
title="${e.title}"
|
||||||
|
type="${e.type or os.type_opds}"
|
||||||
|
href="${e.url}" />
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<opensearch:itemsPerPage>${os.items_per_page}</opensearch:itemsPerPage>
|
||||||
|
<opensearch:startIndex>${os.start_index}</opensearch:startIndex>
|
||||||
|
|
||||||
|
<!--!
|
||||||
|
<opensearch:totalResults>${os.total_results}</opensearch:totalResults>
|
||||||
|
<opensearch:Query role="request"
|
||||||
|
searchTerms="${os.search_terms}"
|
||||||
|
startIndex="${os.start_index}" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
<py:for each="e in os.entries">
|
||||||
|
|
||||||
|
<!--! Navigation feed entry -->
|
||||||
|
|
||||||
|
<py:if test="isinstance (e, bs.Cat)">
|
||||||
|
<py:choose>
|
||||||
|
<py:when test="e.rel == '__statusline__'" />
|
||||||
|
|
||||||
|
<py:otherwise>
|
||||||
|
<entry>
|
||||||
|
<updated>${os.now}</updated>
|
||||||
|
<id>${os.qualify (e.url)}</id>
|
||||||
|
<title>${e.title}</title>
|
||||||
|
<!--! according to spec type defaults to text but quickreader doesn't think so -->
|
||||||
|
<content py:if="e.subtitle or e.extra" type="text">${e.subtitle or e.extra}</content>
|
||||||
|
<category py:if="os.format == 'stanza' and e.header" label="${e.header}"
|
||||||
|
scheme="http://lexcycle.com/stanza/header" term="free" />
|
||||||
|
|
||||||
|
<link type="${e.type or os.type_opds}"
|
||||||
|
rel="${opds_relations.get (e.rel, 'subsection')}"
|
||||||
|
href='${e.url}' />
|
||||||
|
|
||||||
|
<py:for each="link in e.links">
|
||||||
|
<link type="${link.type}"
|
||||||
|
rel="${opds_relations.get (link.rel, 'subsection')}"
|
||||||
|
title="${link.title}"
|
||||||
|
length="${link.length}"
|
||||||
|
href="${link.url}" />
|
||||||
|
</py:for>
|
||||||
|
</entry>
|
||||||
|
</py:otherwise>
|
||||||
|
</py:choose>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<!--! Acquisition feed entry -->
|
||||||
|
|
||||||
|
<py:if test="isinstance (e, bs.DC)">
|
||||||
|
<entry>
|
||||||
|
<updated>${os.now}</updated>
|
||||||
|
<title>${re.sub (r'[\r\n].*', '', e.title)}</title>
|
||||||
|
|
||||||
|
<content type="xhtml">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" >
|
||||||
|
<py:choose test="e.image_flags">
|
||||||
|
<p py:when="3">This edition has images.</p>
|
||||||
|
<p py:when="2">This edition had all images removed.</p>
|
||||||
|
</py:choose>
|
||||||
|
<py:for each="marc in e.marcs">
|
||||||
|
<py:choose test="">
|
||||||
|
<p py:when="marc.code[0]=='5'">
|
||||||
|
<?python
|
||||||
|
text = gg.xmlspecialchars (marc.text)
|
||||||
|
text = re.sub (r'(//\S+)', r'<a href="\1">\1</a>', text)
|
||||||
|
text = re.sub (r'#(\d+)',
|
||||||
|
r'<a href="/ebooks/\1.bibrec.mobile">#\1</a>', text)
|
||||||
|
?>
|
||||||
|
${marc.caption}:
|
||||||
|
${ Markup (gg.insert_breaks (text)) }
|
||||||
|
</p>
|
||||||
|
<p py:otherwise="">
|
||||||
|
${marc.caption}:
|
||||||
|
${ Markup (gg.insert_breaks (gg.xmlspecialchars (marc.text))) }
|
||||||
|
</p>
|
||||||
|
</py:choose>
|
||||||
|
</py:for>
|
||||||
|
<p py:for="author in e.authors">${author.role}: ${author.name_and_dates}</p>
|
||||||
|
<p>Ebook No.: ${e.project_gutenberg_id}</p>
|
||||||
|
<p>Published: ${e.hr_release_date}</p>
|
||||||
|
<p>Downloads: ${e.downloads}</p>
|
||||||
|
<p py:for="language in e.languages">Language: ${language.language}</p>
|
||||||
|
<p py:for="subject in e.subjects">Subject: ${subject.subject}</p>
|
||||||
|
<p py:for="locc in e.loccs">LoCC: ${locc.locc}</p>
|
||||||
|
<p py:for="category in e.categories">Category: ${category}</p>
|
||||||
|
<p>Rights: ${e.rights}</p>
|
||||||
|
</div>
|
||||||
|
</content>
|
||||||
|
|
||||||
|
<id>urn:gutenberg:${e.project_gutenberg_id}:${e.image_flags}</id>
|
||||||
|
<published>${e.xsd_release_date_time}</published>
|
||||||
|
<rights>${e.rights}</rights>
|
||||||
|
|
||||||
|
<py:for each="author in reversed (e.authors)">
|
||||||
|
<author py:if="author.marcrel in ('cre', 'aut')">
|
||||||
|
<name>${author.name}</name>
|
||||||
|
</author>
|
||||||
|
|
||||||
|
<contributor py:if="author.marcrel not in ('cre', 'aut')">
|
||||||
|
<name>${author.name}</name>
|
||||||
|
</contributor>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<category py:if="os.format == 'stanza' and e.header"
|
||||||
|
scheme="http://lexcycle.com/stanza/header"
|
||||||
|
term="free"
|
||||||
|
label="${e.header}" />
|
||||||
|
<category py:for="subject in e.subjects"
|
||||||
|
scheme="http://purl.org/dc/terms/LCSH"
|
||||||
|
term="${subject.subject}" />
|
||||||
|
<category py:for="locc in e.loccs"
|
||||||
|
scheme="http://purl.org/dc/terms/LCC"
|
||||||
|
term="${locc.id}"
|
||||||
|
label="${locc.locc}" />
|
||||||
|
<category py:for="category in e.categories"
|
||||||
|
scheme="http://purl.org/dc/terms/DCMIType"
|
||||||
|
term="$category" />
|
||||||
|
|
||||||
|
<dcterms:language py:for="language in e.languages">${language.id}</dcterms:language>
|
||||||
|
|
||||||
|
<py:for each="marc in e.marcs">
|
||||||
|
<dcterms:identifier py:if="marc.code == '010'">urn:lccn:${marc.text}</dcterms:identifier>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<relevance:score py:if="hasattr (e, 'score')">${e.score}</relevance:score>
|
||||||
|
|
||||||
|
<py:for each="link in e.links">
|
||||||
|
<link type="${link.type}"
|
||||||
|
rel="${opds_relations.get (link.rel, 'acquisition')}"
|
||||||
|
title="${link.title}"
|
||||||
|
length="${link.length}"
|
||||||
|
href="${link.url}" />
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<link type="${os.type_opds}"
|
||||||
|
rel="related"
|
||||||
|
href="${os.url ('also', id = e.project_gutenberg_id)}"
|
||||||
|
title="Readers also downloaded…" />
|
||||||
|
|
||||||
|
<py:for each="author in e.authors">
|
||||||
|
<link type="${os.type_opds}"
|
||||||
|
rel="related"
|
||||||
|
href="${os.url ('author', id = author.id)}"
|
||||||
|
title="${_('By {author}').format (author = author.name)}…"/>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<py:for each="subject in e.subjects">
|
||||||
|
<link type="${os.type_opds}"
|
||||||
|
rel="related"
|
||||||
|
href="${os.url ('subject', id = subject.id)}"
|
||||||
|
title="${_('On {subject}').format (subject = subject.subject)}…"/>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
<py:for each="bookshelf in e.bookshelves">
|
||||||
|
<link type="${os.type_opds}"
|
||||||
|
rel="related"
|
||||||
|
href="${os.url ('bookshelf', id = bookshelf.id)}"
|
||||||
|
title="${_('In {bookshelf}').format (bookshelf = bookshelf.bookshelf)}…"/>
|
||||||
|
</py:for>
|
||||||
|
</entry>
|
||||||
|
</py:if>
|
||||||
|
</py:for>
|
||||||
|
|
||||||
|
</feed>
|
|
@ -0,0 +1,257 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xmlns:og="http://opengraphprotocol.org/schema/"
|
||||||
|
xmlns:fb="http://www.facebook.com/2008/fbml"
|
||||||
|
py:strip="">
|
||||||
|
|
||||||
|
<py:def function="site_head">
|
||||||
|
<style type="text/css">
|
||||||
|
.icon { background: transparent url(/pics/sprite.png?${cherrypy.config['css_mtime']}) 0 0 no-repeat; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css"
|
||||||
|
href="/css/pg-desktop-one.css?${cherrypy.config['css_mtime']}" />
|
||||||
|
|
||||||
|
<!--! IE8 does not recognize application/javascript -->
|
||||||
|
<script type="text/javascript">//<![CDATA[
|
||||||
|
var json_search = "${os.json_search}";
|
||||||
|
var mobile_url = "${os.mobile_url}";
|
||||||
|
var canonical_url = "${os.canonical_url}";
|
||||||
|
var lang = "${os.lang}";
|
||||||
|
var fb_lang = "${os.fb_lang}"; /* FB accepts only xx_XX */
|
||||||
|
var msg_load_more = "${_('Load More Results…')}";
|
||||||
|
var page_mode = "${os.page_mode}";
|
||||||
|
var dialog_title = "${os.user_dialog[1]}";
|
||||||
|
var dialog_message = "${os.user_dialog[0]}";
|
||||||
|
//]]></script>
|
||||||
|
|
||||||
|
<script type="text/javascript"
|
||||||
|
src="/js/pg-desktop-one.js?${cherrypy.config['js_mtime']}" />
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/pics/favicon" />
|
||||||
|
<link rel="canonical" href="${os.canonical_url}" />
|
||||||
|
|
||||||
|
<link rel="search"
|
||||||
|
type="application/opensearchdescription+xml"
|
||||||
|
title="Search Project Gutenberg"
|
||||||
|
href="${os.osd_url}" />
|
||||||
|
|
||||||
|
<link rel="alternate nofollow"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
title="OPDS feed"
|
||||||
|
href="${os.url_carry (format = 'opds')}" />
|
||||||
|
|
||||||
|
<link py:if="os.touch_icon" rel="apple-touch-icon" href="${os.touch_icon}" />
|
||||||
|
<link py:if="os.touch_icon_precomposed" rel="apple-touch-icon-precomposed" href="${os.touch_icon_precomposed}" />
|
||||||
|
<meta py:if="os.viewport" name="viewport" content="${os.viewport}" />
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||||
|
<meta http-equiv="Content-Language" content="${os.lang}" />
|
||||||
|
|
||||||
|
<!--! plain old metadata -->
|
||||||
|
<meta name="title" content="${os.title}" />
|
||||||
|
<meta name="description" content="${os.description}" />
|
||||||
|
<meta name="keywords" content="ebook, ebooks, free ebooks, free books, book, books, audio books" />
|
||||||
|
<meta name="classification" content="public" />
|
||||||
|
|
||||||
|
<!--! facebook open graph -->
|
||||||
|
<meta property="og:title" content="${os.title}" />
|
||||||
|
<meta property="og:description" content="${os.description}" />
|
||||||
|
<meta property="og:type" content="${os.og_type}" />
|
||||||
|
<meta property="og:image" content="${os.snippet_image_url}" />
|
||||||
|
<meta property="og:url" content="${os.canonical_url}" />
|
||||||
|
<meta property="og:site_name" content="Project Gutenberg" />
|
||||||
|
<meta property="fb:app_id" content="${cherrypy.config['facebook_app_id']}" />
|
||||||
|
<!--! <meta property="fb:admins" content="615269807" /> -->
|
||||||
|
|
||||||
|
<!--! twitter card -->
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:site" content="@gutenberg_new" />
|
||||||
|
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="credits">
|
||||||
|
<div id="credits">
|
||||||
|
<div class="h1"
|
||||||
|
title="Credits">Credits</div>
|
||||||
|
|
||||||
|
<a href="http://www.ibiblio.org" rel="nofollow"
|
||||||
|
title="Project Gutenberg is hosted by ibiblio.">
|
||||||
|
<span class ="icon icon_hosted" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p>This web site uses only free software.</p>
|
||||||
|
|
||||||
|
<a href="http://httpd.apache.org/" rel="nofollow"
|
||||||
|
title="Powered by Apache">
|
||||||
|
<span class="icon icon_apache" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="http://www.python.org" rel="nofollow"
|
||||||
|
title="Powered by Python">
|
||||||
|
<span class="icon icon_python" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="http://www.postgresql.org" rel="nofollow"
|
||||||
|
title="Powered by PostgreSQL">
|
||||||
|
<span class="icon icon_postgres" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="copyright">
|
||||||
|
<div class="copyright" i18n:comment="The copyright notice on the footer of every page.">
|
||||||
|
© 2003–2012 Project Gutenberg Literary Archive Foundation — All Rights Reserved.
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="site_top">
|
||||||
|
<div id="fb-root" />
|
||||||
|
|
||||||
|
<div id="print-head" class="noscreen">
|
||||||
|
<div class="center">http:${os.desktop_url}<br/><br/>${os.tagline}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="screen-head" class="noprint">
|
||||||
|
<table>
|
||||||
|
<tr i18n:comment="The logo, tagline and badges at the very top of every page.">
|
||||||
|
|
||||||
|
<td rowspan="2" id="logo" i18n:comment="The PG logo at the top left of every page.">
|
||||||
|
<a href="/wiki/Main_Page" title="Go to the Main Page.">
|
||||||
|
<span id="${cherrypy.tools.rate_limiter.get_challenge ()}" class="icon icon_logo" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td id="tagline-badges" colspan="2">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td id="tagline">${os.tagline}</td>
|
||||||
|
<td id="paypal-badge" class="badge">${paypal ()}</td>
|
||||||
|
<td id="flattr-badge" class="badge">${flattr ()}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr id="menubar-search">
|
||||||
|
<td id="menubar" i18n:comment="The menu bar at the top of every page.">
|
||||||
|
<a py:if="os.page != 'start'"
|
||||||
|
id="menubar-first"
|
||||||
|
tabindex="11" accesskey="1"
|
||||||
|
title="Start a new search."
|
||||||
|
href="${os.url ('start')}">Search</a>
|
||||||
|
<span py:if="os.page == 'start'"
|
||||||
|
id="menubar-first" class="grayed">Search</span>
|
||||||
|
|
||||||
|
<a tabindex="22"
|
||||||
|
title="Our latest releases."
|
||||||
|
href="/ebooks/search/?sort_order=release_date">Latest</a>
|
||||||
|
|
||||||
|
<a tabindex="31"
|
||||||
|
title="Read the Project Gutenberg terms of use."
|
||||||
|
href="/terms_of_use/">Terms of Use</a>
|
||||||
|
|
||||||
|
<a tabindex="32"
|
||||||
|
href="/wiki/Gutenberg:Project_Gutenberg_Needs_Your_Donation"
|
||||||
|
title="Learn why we need some money.">Donate?</a>
|
||||||
|
|
||||||
|
<a tabindex="33" accesskey="m" href="${os.mobile_url}"
|
||||||
|
title="Go to our mobile site.">Mobile</a>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td id="search" i18n:comment="The search box at the top right of every page.">
|
||||||
|
<form method="get" action="${os.desktop_search}"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
<table class="borderless">
|
||||||
|
<tr>
|
||||||
|
<td id="search-button-cell">
|
||||||
|
<button id="search-button" type="submit" title="Execute the search. <enter>">
|
||||||
|
<span class="icon icon_smsearch" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td id="search-input-cell">
|
||||||
|
<input id="search-input" name="query" type="text" title="${os.placeholder} <s>"
|
||||||
|
accesskey="s" value="${os.search_terms}" />
|
||||||
|
</td>
|
||||||
|
<td id="help-button-cell">
|
||||||
|
<button id="help-button" type="button" title="Open the help menu. <h>"
|
||||||
|
accesskey="h">Help</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="helpbox" class="hide">
|
||||||
|
<xi:include href="help.html" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="site_footer">
|
||||||
|
<div class="footer" i18n:comment="On the footer of every page.">
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="prev_next_links">
|
||||||
|
<span class="links" i18n:comment="The links to move between paginated search results."
|
||||||
|
xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/">
|
||||||
|
|
||||||
|
<py:if test="os.show_prev_page_link">|
|
||||||
|
<a title="Go to the first page of results."
|
||||||
|
accesskey="a"
|
||||||
|
href="${os.url_carry (start_index = 1)}">First</a>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<py:if test="os.show_prev_page_link">|
|
||||||
|
<a title="Go to the previous page of results."
|
||||||
|
accesskey="-"
|
||||||
|
href="${os.url_carry (start_index = os.prev_page_index)}">Previous</a>
|
||||||
|
</py:if>
|
||||||
|
|
||||||
|
<py:if test="os.show_next_page_link">|
|
||||||
|
<a title="Go to the next page of results."
|
||||||
|
accesskey="+"
|
||||||
|
href="${os.url_carry (start_index = os.next_page_index)}">Next</a>
|
||||||
|
</py:if>
|
||||||
|
</span>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="paypal">
|
||||||
|
<form class="paypal-button"
|
||||||
|
action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
||||||
|
<div>
|
||||||
|
<!--! xml:lang avoids extraction for translation -->
|
||||||
|
<input xml:lang="en" type="hidden" name="cmd" value="_s-xclick" />
|
||||||
|
<input xml:lang="en" type="hidden" name="hosted_button_id" value="XKAL6BZL3YPSN" />
|
||||||
|
<input type="image" name="submit"
|
||||||
|
src="//${cherrypy.config['file_host']}/pics/paypal/${os.paypal_lang}.gif"
|
||||||
|
title="Send us money through PayPal." />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="flattr">
|
||||||
|
<a class="flattr-button" target="_blank"
|
||||||
|
href="https://flattr.com/thing/509045/Project-Gutenberg"
|
||||||
|
title="Send us money through Flattr.">
|
||||||
|
<span class="icon icon_flattr" />
|
||||||
|
</a>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,121 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
py:strip="">
|
||||||
|
|
||||||
|
<py:def function="site_head">
|
||||||
|
<style type="text/css">
|
||||||
|
.icon { background: transparent url(/pics/sprite.png?${cherrypy.config['css_mtime']}) 0 0 no-repeat; }
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" type="text/css"
|
||||||
|
href="/css/pg-mobile-one.css?${cherrypy.config['css_mtime']}" />
|
||||||
|
|
||||||
|
<script type="application/javascript"><![CDATA[
|
||||||
|
var mobile_search = "${os.add_amp (os.mobile_search)}query=";
|
||||||
|
var json_search = "${os.json_search}";
|
||||||
|
var msg_load_more = "${_('Load More Results…')}";
|
||||||
|
var do_animations = ${'true' if os.do_animations else 'false'};
|
||||||
|
]]>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="/js/pg-mobile-one.js?${cherrypy.config['js_mtime']}" />
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" />
|
||||||
|
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||||
|
<meta http-equiv="Content-Language" content="${os.lang}" />
|
||||||
|
|
||||||
|
<meta name="description" content="The Project Gutenberg ebook catalog for mobile devices." />
|
||||||
|
<meta name="keywords" content="free ebooks, free books, free audio books" />
|
||||||
|
<meta name="classification" content="public" />
|
||||||
|
|
||||||
|
<link rel="icon" href="/pics/favicon" />
|
||||||
|
|
||||||
|
<link rel="canonical" href="${os.canonical_url}" />
|
||||||
|
|
||||||
|
<link rel="search"
|
||||||
|
type="application/opensearchdescription+xml"
|
||||||
|
title="Search Project Gutenberg"
|
||||||
|
href="${os.osd_url}" />
|
||||||
|
|
||||||
|
<link rel="alternate nofollow"
|
||||||
|
type="${os.type_opds}"
|
||||||
|
title="OPDS feed"
|
||||||
|
href="${os.url_carry (format = 'opds')}" />
|
||||||
|
|
||||||
|
<link py:if="os.touch_icon" rel="apple-touch-icon" href="${os.touch_icon}" />
|
||||||
|
<link py:if="os.touch_icon_precomposed" rel="apple-touch-icon-precomposed" href="${os.touch_icon_precomposed}" />
|
||||||
|
<meta py:if="os.viewport" name="viewport" content="${os.viewport}" />
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="search_box">
|
||||||
|
<li class="grayed" id="searchlist">
|
||||||
|
<div class="table link">
|
||||||
|
<div class="row">
|
||||||
|
<div class="cell leftcell">
|
||||||
|
<div class="icon icon_search" id="${cherrypy.tools.rate_limiter.get_challenge ()}" />
|
||||||
|
</div>
|
||||||
|
<div class="cell content">
|
||||||
|
<form id="search" method="get" action="${os.mobile_search}"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
<div id="query-clear-wrapper">
|
||||||
|
<div id="query-wrapper">
|
||||||
|
<input id="query" name="query"
|
||||||
|
type="text"
|
||||||
|
inputmode="latin"
|
||||||
|
value="${os.search_terms}"
|
||||||
|
title="${os.placeholder}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!--! <div class="cell cancelcell">
|
||||||
|
<button type="reset" id="clear" class="icon icon_cancel"
|
||||||
|
title="Clear" i18n:comment="Reset Form Button" />
|
||||||
|
</div> -->
|
||||||
|
<div class="cell rightcell">
|
||||||
|
<button type="button" id="help"
|
||||||
|
title="Help" onclick="toggle_help ()" i18n:comment="Help about search button">
|
||||||
|
<div class="icon icon_help" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="grayed" id="helpbox"
|
||||||
|
style="display: none" onclick="clear_help ()">
|
||||||
|
<div class="helpbox">
|
||||||
|
<xi:include href="help.html" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<py:if test="os.page != 'start' and os.start_index == 1">
|
||||||
|
<li class="navlink grayed">
|
||||||
|
<a class="table link" href="${os.url ('start')}" accesskey="h">
|
||||||
|
<span class="row">
|
||||||
|
<span class="cell leftcell">
|
||||||
|
<span class="icon icon_internal" />
|
||||||
|
</span>
|
||||||
|
<span class="cell content">
|
||||||
|
<span class="title">Search Start Page</span>
|
||||||
|
</span>
|
||||||
|
<span class="cell rightcell">
|
||||||
|
<span class="icon icon_next" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</py:if>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="site_footer">
|
||||||
|
<div class="footer">
|
||||||
|
<div class="copyright">
|
||||||
|
© 2003–2012 Project Gutenberg Literary Archive Foundation — All Rights Reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--! support.google.com/webmasters/bin/answer.py?hl=en&answer=71453&topic=8476&ctx=topic -->
|
||||||
|
|
||||||
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
>
|
||||||
|
|
||||||
|
<sitemap py:for="s in data.sitemaps">
|
||||||
|
<loc>${s.loc}</loc>
|
||||||
|
<lastmod>${s.lastmod}</lastmod>
|
||||||
|
</sitemap>
|
||||||
|
|
||||||
|
</sitemapindex>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--! support.google.com/webmasters/bin/answer.py?hl=en&answer=183668&topic=8476&ctx=topic -->
|
||||||
|
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
>
|
||||||
|
|
||||||
|
<url py:for="u in data.urls">
|
||||||
|
<loc>${u.loc}</loc>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
</urlset>
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xmlns:og="http://opengraphprotocol.org/schema/"
|
||||||
|
py:strip="">
|
||||||
|
|
||||||
|
<?python
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
def p (params):
|
||||||
|
return urllib.parse.urlencode (params).replace ('+', '%20')
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
<py:def function="fb_share(url, title, description, picture)">
|
||||||
|
<?python
|
||||||
|
params = {
|
||||||
|
'link': url,
|
||||||
|
'app_id': cherrypy.config['facebook_app_id'],
|
||||||
|
'name': title,
|
||||||
|
'description': description,
|
||||||
|
'redirect_uri': 'https://www.gutenberg.org/fb_redirect.html',
|
||||||
|
}
|
||||||
|
if picture is not None:
|
||||||
|
params['picture'] = picture
|
||||||
|
?>
|
||||||
|
<div class="social-button fb-share-button" i18n:comment="Share on Facebook.">
|
||||||
|
<a href="https://www.facebook.com/dialog/feed?${p (params)}"
|
||||||
|
title="Share on Facebook"
|
||||||
|
onclick="open_share_popup(this.href, this.target, 1024, 560)" target="_fb_share_popup">
|
||||||
|
<span class="icon icon_facebook" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
|
||||||
|
<py:def function="gplus_share(url)">
|
||||||
|
<!-- share without javascript -->
|
||||||
|
<?python
|
||||||
|
params = {
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="social-button gplus-share-button">
|
||||||
|
<!--! https://developers.google.com/+/web/share/#sharelink -->
|
||||||
|
<a href="https://plus.google.com/share?${p (params)}"
|
||||||
|
title="Share on Google+"
|
||||||
|
onclick="open_share_popup(this.href, this.target, 640, 320)"
|
||||||
|
target="_gplus_share_popup">
|
||||||
|
<span class="icon icon_gplus" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
|
||||||
|
<py:def function="tw_share(url, text)">
|
||||||
|
<!-- tweet without javascript -->
|
||||||
|
<?python
|
||||||
|
params = {
|
||||||
|
'url': url,
|
||||||
|
'text': text.encode ('utf-8'),
|
||||||
|
'count': 'none',
|
||||||
|
'lang': os.twitter_lang,
|
||||||
|
'related': "gutenberg_new:Project Gutenberg New Books"
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="social-button twitter-share-button">
|
||||||
|
<!--! https://dev.twitter.com/docs/tweet-button -->
|
||||||
|
<a href="https://twitter.com/share?${p (params)}"
|
||||||
|
title="Share on Twitter"
|
||||||
|
onclick="open_share_popup(this.href, this.target, 640, 320)"
|
||||||
|
target="_tw_share_popup">
|
||||||
|
<span class="icon icon_twitter" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
|
||||||
|
<py:def function="rss_follow()">
|
||||||
|
<div class="social-button rss-follow-button">
|
||||||
|
<a href="/feeds/today.rss"
|
||||||
|
title="Subscribe to the New Books RSS feed.">
|
||||||
|
<span class="icon icon_rss" />
|
||||||
|
<span class="alt" i18n:comment="Subscribe to RSS feed.">Subscribe</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,165 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xmlns:py="http://genshi.edgewall.org/"
|
||||||
|
xmlns:i18n="http://genshi.edgewall.org/i18n"
|
||||||
|
xml:lang="en">
|
||||||
|
|
||||||
|
<!--! xml:lang avoids extraction for translation -->
|
||||||
|
|
||||||
|
<?python
|
||||||
|
from libgutenberg.GutenbergGlobals import xmlspecialchars as esc
|
||||||
|
?>
|
||||||
|
|
||||||
|
<py:def function="print_session">
|
||||||
|
<?python
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
hostname = ''
|
||||||
|
whois = ''
|
||||||
|
tags = []
|
||||||
|
bad_tags = []
|
||||||
|
|
||||||
|
ips = d.ips
|
||||||
|
info = ips.ipinfo
|
||||||
|
if info is not None:
|
||||||
|
hostname = info.hostname
|
||||||
|
whois = ' - '.join (filter (None, info.whois.values ()))
|
||||||
|
if info.is_dialup ():
|
||||||
|
tags.append ('DUL')
|
||||||
|
bad_tags = sorted (info.tags & info.blackhat_tags)
|
||||||
|
|
||||||
|
if d['blocked'] == 9:
|
||||||
|
tags.append ('->DB')
|
||||||
|
if d['css_ok'] == False:
|
||||||
|
bad_tags.append ('NO_CSS')
|
||||||
|
session_cnt = len (d.sessions)
|
||||||
|
?>
|
||||||
|
<span class='ip'>${str (ips)}</span>
|
||||||
|
-
|
||||||
|
<span class='hits'>${d['hits']}</span> hits
|
||||||
|
<span py:if="d['dhits'] > 0"><span class='dhits'>${d['dhits']}</span> denied</span>
|
||||||
|
<span class='rhits'>(${d['rhits']}/${d['rhits_max']})</span>
|
||||||
|
<span py:if='session_cnt > 1' class='sessions'>${session_cnt} sess.</span>
|
||||||
|
<span class='host'>${hostname}</span>
|
||||||
|
<span class='tags'>${' '.join (sorted (tags))}</span>
|
||||||
|
<span class='bad_tags'>${' '.join (sorted (bad_tags))}</span>
|
||||||
|
<span class='whois'>${whois}</span>
|
||||||
|
<span class="ua">${' -- '.join (sorted (
|
||||||
|
[rl.ua_decode (ua) for ua in d['user_agents']]
|
||||||
|
))[:1000]}</span>
|
||||||
|
<span class="ua-sig">${' -- '.join (sorted (
|
||||||
|
[rl.ua_decode (sig) for sig in d['signatures']]
|
||||||
|
))[:1000]}</span>
|
||||||
|
<a href="/webmaster/stats/block/?ip=${ips.get_ip_to_block ()}">block</a>
|
||||||
|
<a href="/webmaster/stats/unblock/?ip=${ips.get_ip_to_block ()}">unblock</a>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<py:def function="print_requests">
|
||||||
|
<ul class="requests">
|
||||||
|
<li py:for="request in d['requests']">${request}</li>
|
||||||
|
</ul>
|
||||||
|
</py:def>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>AppServer Status</title>
|
||||||
|
<style type="text/css">
|
||||||
|
.ip { font-weight: bold }
|
||||||
|
.hits { font-weight: bold }
|
||||||
|
.dhits { font-weight: bold; color: red }
|
||||||
|
.rhits { color: blue }
|
||||||
|
.host { font-weight: bold }
|
||||||
|
.sessions { font-weight: bold; color: red }
|
||||||
|
.whois { color: grey }
|
||||||
|
.tags { font-weight: bold; color: green }
|
||||||
|
.bad_tags { font-weight: bold; color: red }
|
||||||
|
.ua { color: blue }
|
||||||
|
.ua-sig { color: #808 }
|
||||||
|
.requests { list-style-type: none; font-size: small; color: grey }
|
||||||
|
.requests li { display: inline }
|
||||||
|
.requests li:before { content: ' - ' }
|
||||||
|
.whitelisted { color: green }
|
||||||
|
.nagios { display: none }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Stats</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
RHits Max: ${rl.rhits_max}.
|
||||||
|
Captchas: ${rl.captchas_max}.
|
||||||
|
Cleanup Frequency: ${rl.frequency}s.
|
||||||
|
</p>
|
||||||
|
<p>Active Users: ${rl.users}</p>
|
||||||
|
<p>Hits: ${rl.hits}</p>
|
||||||
|
<p>Whitelisted: ${rl.whitelist_hits}</p>
|
||||||
|
<p>Denied: ${rl.denied_hits}</p>
|
||||||
|
<p>Blocked IPs: ${len (blocked)}</p>
|
||||||
|
<p>Total Backends: ${backends}</p>
|
||||||
|
<p>Active Backends: ${active_backends}</p>
|
||||||
|
|
||||||
|
<p>Formats Sum:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr py:for="key, value, percent in bs.formats_sum_acc.iter_results ()">
|
||||||
|
<td>${key}</td><td>${value}</td><td>${"%.3f" % percent}</td>
|
||||||
|
<td class="nagios">nagios-${key}-sum: ${value} ${"%.3f" % percent}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Formats Detail:</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr py:for="key, value, percent in bs.formats_acc.iter_results ()">
|
||||||
|
<td>${key}</td><td>${value}</td><td>${"%.3f" % percent}</td>
|
||||||
|
<td class="nagios">nagios-${key}: ${value} ${"%.3f" % percent}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<py:if test="resolve">
|
||||||
|
|
||||||
|
<p>Blocked IPs:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<!--! the blocked IPs sorted by IP -->
|
||||||
|
<li py:for="d in blocked">
|
||||||
|
${print_session ()}
|
||||||
|
${print_requests ()}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Busiest IPs:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<!--! the top 10 active IPs sorted by hits desc -->
|
||||||
|
<li py:for="d in busiest" class="${'whitelisted' if d.ips.whitelisted else ''}">
|
||||||
|
${print_session ()}
|
||||||
|
${print_requests ()}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>IPs with most sessions:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<!--! the top 10 IPs with most sessions sorted by sessions desc -->
|
||||||
|
<li py:for="d in most_sessions" class="${'whitelisted' if d.ips.whitelisted else ''}">
|
||||||
|
${print_session ()}
|
||||||
|
${print_requests ()}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Active IPs: ${len (active)}</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<!--! the active IPs sorted by IP -->
|
||||||
|
<li py:for="d in active" class="${'whitelisted' if d.ips.whitelisted else ''}">
|
||||||
|
${print_session ()}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</py:if> <!--! resolve -->
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue