Browse Source

supports custom scripts.

Tongyanqun 1 year ago
parent
commit
593c848330

+ 2 - 0
app/src/app.html

@@ -7,6 +7,7 @@
 
 <body>
 {{ APP }}
+<!-- Custom Scripts Start -->
 <!-- Matomo -->
 <script>
   var _paq = window._paq = window._paq || [];
@@ -22,6 +23,7 @@
   })();
 </script>
 <!-- End Matomo Code -->
+<!-- Custom Scripts End -->
 </body>
 
 </html>

+ 53 - 16
app/src/pages/admin/settings.vue

@@ -1,4 +1,4 @@
-<template>
+<template xmlns="http://www.w3.org/1999/html">
     <div>
         <v-card class="my-2 elevation-4" v-for="card in cards" :key="card.title">
             <v-card-title @click="card.show = !card.show">
@@ -21,6 +21,10 @@
                                 v-model="settings[f.key]" :key="f.key" :label="f.label"></v-checkbox>
                     <v-textarea outlined v-else-if="f.type === 'textarea' " :prepend-icon="f.icon"
                                 v-model="settings[f.key]" :key="f.key" :label="f.label"></v-textarea>
+                    <p v-else-if="f.type === 'plaintext'">
+                        <v-icon>{{ f.icon }}</v-icon>
+                        <b>{{ f.value }}</b>
+                    </p>
                     <v-text-field v-else :prepend-icon="f.icon" v-model="settings[f.key]" :key="f.key" :label="f.label"
                                   type="text"></v-text-field>
                 </template>
@@ -69,7 +73,8 @@
 
                 <template v-if="card.show_socials">
                     <p>所启用的社交网络将会在登录页面自动显示按钮。</p>
-                    <v-combobox v-model="settings.SOCIALS" :items="sns_items" label="选择要启用的社交网络账号" hide-selected
+                    <v-combobox v-model="settings.SOCIALS" :items="sns_items" label="选择要启用的社交网络账号"
+                                hide-selected
                                 multiple small-chips>
                         <template v-slot:selection="{ attrs, item, parent, selected }">
                             <v-chip v-bind="attrs" color="green lighten-3" :input-value="selected" label small>
@@ -105,6 +110,10 @@
 
         <br/>
         <div class="text-center">
+            <p style="{color: #ff5449ff}">保存配置后会自动重启服务端,请耐心等待服务端重启完成后刷新页面!<br/>
+                如果修改了自定义脚本,则需要等待脚本编译完成后才可以访问,服务约中断3分钟。
+            </p>
+            <br/>
             <v-btn color="primary" @click="save_settings">保存</v-btn>
         </div>
     </div>
@@ -164,8 +173,18 @@ export default {
                 title: "用户设置",
                 fields: [
                     {icon: "", key: "ALLOW_GUEST_READ", label: "允许访客在线阅读(无需注册和登录)", type: 'checkbox'},
-                    {icon: "", key: "ALLOW_GUEST_DOWNLOAD", label: "允许任意下载(访客无需注册和登录)", type: 'checkbox'},
-                    {icon: "", key: "ALLOW_GUEST_PUSH", label: "允许任意推送Kindle(访客无需注册和登录)", type: 'checkbox'},
+                    {
+                        icon: "",
+                        key: "ALLOW_GUEST_DOWNLOAD",
+                        label: "允许任意下载(访客无需注册和登录)",
+                        type: 'checkbox'
+                    },
+                    {
+                        icon: "",
+                        key: "ALLOW_GUEST_PUSH",
+                        label: "允许任意推送Kindle(访客无需注册和登录)",
+                        type: 'checkbox'
+                    },
 
                 ],
                 groups: [
@@ -204,7 +223,11 @@ export default {
                 title: "邮件服务",
                 subtitle: '邮箱注册、推送邮箱依赖此配置(SMTP服务器地址可带端口,或者不带端口,默认为465号)',
                 fields: [
-                    {icon: "email", key: "smtp_server", label: "SMTP服务器(例如 smtp.mailgun.org 或者 smtp.gmail.com:587)"},
+                    {
+                        icon: "email",
+                        key: "smtp_server",
+                        label: "SMTP服务器(例如 smtp.mailgun.org 或者 smtp.gmail.com:587)"
+                    },
                     {icon: "person", key: "smtp_username", label: "发件人邮箱地址(例如 user@gmail.com)"},
                     {icon: "lock", key: "smtp_password", label: "邮箱密码"},
                 ],
@@ -223,15 +246,30 @@ export default {
                 title: "高级配置项",
                 fields: [
                     {icon: "home", key: "static_host", label: "CDN域名"},
-                    // 后续可以修改为choice下拉框选项
-                    {icon: "info", key: "BOOK_NAMES_FORMAT", label: "目录和文件名模式(utf8为保留原始中文,en表示拼音英文)"},
+                    {
+                        icon: "info",
+                        key: "BOOK_NAMES_FORMAT",
+                        label: "目录和文件名模式(utf8为保留原始中文,en表示拼音英文)"
+                    },
                     {icon: "info", key: "avatar_service", label: "可使用www.gravatar.com或cravatar.cn头像服务"},
                     {icon: "info", key: "MAX_UPLOAD_SIZE", label: "文件上传字节数限制(例如100MB或100KB)"},
                     {icon: "info", key: "douban_baseurl", label: "豆瓣插件API地址(例如 http://10.0.0.1:8080 )"},
                     {icon: "info", key: "douban_max_count", label: "豆瓣插件API查询结果数量"},
                     {icon: "lock", key: "cookie_secret", label: "COOKIE随机密钥"},
                     {icon: "info", key: "scan_upload_path", label: "批量导入扫描目录"},
-                    {icon: "", key: "autoreload", label: "更新配置后自动重启服务器(首次开启需人工重启)", type: 'checkbox'},
+                    {
+                        icon: "info",
+                        type: "plaintext",
+                        value: "自定义运行脚本。如需站点统计,可在此处设置。请注意,脚本语法不正确,可能会导致服务端无法启动。" +
+                            "如遇到服务端无法启动的情况,可以从后台修改/data/books/settings/auto.py文件中的custom_scripts和scipts_compiled参数为如下值:" +
+                            "\"custom_scripts\":\"\", \"scipts_compiled\":False," +
+                            "然后重启容器即可恢复自定义脚本为空值。重建容器时,首次启动容器,也需要重新编译脚本,约需耗时2分钟。"
+                    },
+                    {
+                        key: "custom_scripts",
+                        label: "",
+                        type: 'textarea'
+                    },
                 ],
                 tips: [
                     {
@@ -255,14 +293,13 @@ export default {
             this.$backend("/admin/settings", {
                 method: 'POST',
                 body: JSON.stringify(this.settings),
-            })
-                .then(rsp => {
-                    if (rsp.err !== 'ok') {
-                        this.$alert('error', rsp.msg);
-                    } else {
-                        this.$alert('success', '保存成功!可能需要5~10秒钟生效!');
-                    }
-                });
+            }).then(rsp => {
+                if (rsp.err !== 'ok') {
+                    this.$alert('error', rsp.msg);
+                } else {
+                    this.$alert('success', '保存成功!可能需要5~10秒钟生效!');
+                }
+            });
         },
         show_sns_config: function (s) {
             var msg = `请前往${s.text}的 <a :href="${s.link}" target="_blank">配置页面</a> 获取密钥,并设置回调地址(callback URL)为

+ 18 - 0
app/src/static/static/pdfjs/web/viewer.html

@@ -35,6 +35,24 @@ See https://github.com/adobe-type-tools/cmap-resources
 
   <script src="viewer.js"></script>
 
+<!-- Custom Scripts Start -->
+<!-- Matomo -->
+<script>
+  var _paq = window._paq = window._paq || [];
+  /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
+  _paq.push(['trackPageView']);
+  _paq.push(['enableLinkTracking']);
+  (function() {
+    var u="//analytics.codefine.site:6870/";
+    _paq.push(['setTrackerUrl', u+'matomo.php']);
+    _paq.push(['setSiteId', '2']);
+    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+    g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
+  })();
+</script>
+<!-- End Matomo Code -->
+<!-- Custom Scripts End -->
+
   </head>
 
   <body tabindex="1">

+ 2 - 1
app/src/static/static/readium/index.html

@@ -56,7 +56,7 @@
         });
     });
 </script>
-
+<!-- Custom Scripts Start -->
 <!-- Matomo -->
 <script>
   var _paq = window._paq = window._paq || [];
@@ -72,6 +72,7 @@
   })();
 </script>
 <!-- End Matomo Code -->
+<!-- Custom Scripts End -->
 </body>
 
 </html>

+ 32 - 39
webserver/handlers/admin.py

@@ -4,6 +4,7 @@
 import datetime
 import hashlib
 import logging
+import os
 import re
 import ssl
 import subprocess
@@ -15,7 +16,7 @@ from gettext import gettext as _
 
 import tornado
 
-from webserver import loader, utils
+from webserver import loader, utils, settings
 from webserver.handlers.base import BaseHandler, auth, js, is_admin
 from webserver.models import Reader
 from webserver.plugins.meta import baike, douban
@@ -195,6 +196,25 @@ TITLE_TEMPLATE="%%s | %(site_title)s"
 
         # ok, it's safe to update current environment
         CONF["installed"] = True
+
+        if not CONF["scripts_compiled"]:
+            custom_scripts = CONF["custom_scripts"] if "custom_scripts" in CONF else ""
+            settings.replace_scripts("/var/www/talebook/app/src/app.html", custom_scripts)
+            settings.replace_scripts("/var/www/talebook/app/src/static/static/readium/index.html", custom_scripts)
+            settings.replace_scripts("/var/www/talebook/webserver/resources/book/read.html", custom_scripts)
+            settings.replace_scripts("/var/www/talebook/app/src/static/static/pdfjs/web/viewer.html", custom_scripts)
+
+            ret = subprocess.check_call(["npm", "run", "build"], cwd="/var/www/talebook/app/")
+            logging.info("compiled nuxt result: %d" % ret)
+            args["scripts_compiled"] = True
+            CONF["scripts_compiled"] = True
+            try:
+                args.dumpfile()
+            except:
+                logging.error(traceback.format_exc())
+                return {"err": "file.permission", "msg": _(u"更新磁盘配置文件失败!请确保配置文件的权限为可写入!")}
+
+        subprocess.call(["supervisorctl", "reload"])
         return {"err": "ok", "rsp": args}
 
 
@@ -206,7 +226,11 @@ class AdminSettings(BaseHandler):
             return {"err": "permission", "msg": _(u"无权访问此接口")}
 
         sns = [
-            {"value": "qq", "text": "QQ", "link": "https://connect.qq.com/"},
+            {
+                "value": "qq",
+                "text": "QQ",
+                "link": "https://connect.qq.com/"
+            },
             {
                 "value": "amazon",
                 "text": "Amazon",
@@ -234,42 +258,6 @@ class AdminSettings(BaseHandler):
     @auth
     def post(self):
         data = tornado.escape.json_decode(self.request.body)
-        KEYS = [
-            "ALLOW_GUEST_DOWNLOAD",
-            "ALLOW_GUEST_PUSH",
-            "ALLOW_GUEST_READ",
-            "ALLOW_REGISTER",
-            "BOOK_NAMES_FORMAT",
-            "FRIENDS",
-            "FOOTER",
-            "INVITE_CODE",
-            "INVITE_MESSAGE",
-            "INVITE_MODE",
-            "MAX_UPLOAD_SIZE",
-            "RESET_MAIL_CONTENT",
-            "RESET_MAIL_TITLE",
-            "SIGNUP_MAIL_CONTENT",
-            "SIGNUP_MAIL_TITLE",
-            "SOCIALS",
-            "autoreload",
-            "cookie_secret",
-            "scan_upload_path",
-            "RESTRICT_DOWNLOADS_COUNT_PER_IP",
-            "downloads_count_per_ip_limitation",
-            "douban_apikey",
-            "douban_baseurl",
-            "douban_max_count",
-            "site_title",
-            "smtp_password",
-            "smtp_server",
-            "smtp_username",
-            "static_host",
-            "xsrf_cookies",
-            "settings_path",
-            "avatar_service",
-            "google_analytics_id",
-        ]
-
         args = loader.SettingsLoader()
         args.clear()
 
@@ -277,9 +265,14 @@ class AdminSettings(BaseHandler):
             if key.startswith("SOCIAL_AUTH"):
                 if key.endswith("_KEY") or key.endswith("_SECRET"):
                     args[key] = val
-            elif key in KEYS:
+            elif key in settings.KEYS:
                 args[key] = val
 
+        old_custom_scripts = CONF["custom_scripts"] if "custom_scripts" in CONF else None
+        new_custom_scripts = args["custom_scripts"] if "custom_scripts" in args else None
+        if new_custom_scripts != old_custom_scripts:
+            args["scripts_compiled"] = False
+
         logic = SettingsSaverLogic()
         return logic.save_extra_settings(args)
 

+ 1 - 1
webserver/loader.py

@@ -80,7 +80,7 @@ settings = {
         d = self.set_store_path()
         py = os.path.join(d, filename)
         pyc = os.path.join(d, filename + "c")
-        logging.error("saving settings file: %s" % py)
+        logging.info("saving settings file: %s" % py)
         with open(py, "w") as f:
             f.write(code)
         try:

+ 108 - 8
webserver/main.py

@@ -4,7 +4,9 @@
 import logging
 import os
 import re
+import subprocess
 import sys
+import traceback
 from gettext import gettext as _
 
 import tornado.httpserver
@@ -15,7 +17,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
 from tornado import web
 from tornado.options import define, options
 
-from webserver import loader, models, social_routes, handlers
+from webserver import loader, models, social_routes, handlers, settings
 
 CONF = loader.get_settings()
 define("host", default="", type=str, help=_("The host address on which to listen"))
@@ -127,13 +129,6 @@ def make_app():
     logging.info("Init HTML    with [%s]" % CONF["html_path"])
     logging.info("Init Nuxtjs  with [%s]" % CONF["nuxt_env_path"])
 
-    logging.info("updating configs ...")
-    # 触发一次空白配置更新
-    from webserver.handlers.admin import SettingsSaverLogic
-    logic = SettingsSaverLogic()
-    logic.update_nuxtjs_env()
-    logging.info("updating configs done ...")
-
     # build sql session factory
     engine = create_engine(auth_db_path, **CONF["db_engine_args"], pool_size=20, max_overflow=0)
     ScopedSession = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=False))
@@ -182,6 +177,7 @@ def make_app():
             "ScopedSession": ScopedSession,
             "build_time": fromtimestamp(os.stat(path).st_mtime),
             "default_cover": default_cover,
+            "autoreload": False,
         }
     )
 
@@ -207,8 +203,15 @@ def get_upload_size():
     return int(float(s) * n)
 
 
+new_container_lock = "/var/www/talebook/newcontainer.lock"
+
+
 def main():
     tornado.options.parse_command_line()
+    ret = upgrade_new_container()
+    if ret != 0:
+        return ret
+
     app = make_app()
     http_server = tornado.httpserver.HTTPServer(app, xheaders=True, max_buffer_size=get_upload_size())
     http_server.listen(options.port, options.host)
@@ -218,6 +221,103 @@ def main():
     _EngineDebuggingSignalEvents(app._engine, app.import_name).register()
 
 
+def compile_custom_scriots():
+    custom_scripts = CONF["custom_scripts"] if "custom_scripts" in CONF else ""
+    settings.replace_scripts("/var/www/talebook/app/src/app.html", custom_scripts)
+    settings.replace_scripts("/var/www/talebook/app/src/static/static/readium/index.html", custom_scripts)
+    settings.replace_scripts("/var/www/talebook/webserver/resources/book/read.html", custom_scripts)
+    settings.replace_scripts("/var/www/talebook/app/src/static/static/pdfjs/web/viewer.html", custom_scripts)
+
+    ret = subprocess.check_call(["npm", "run", "build"], cwd="/var/www/talebook/app/")
+    logging.info("compiled nuxt result: %d" % ret)
+    if ret != 0:
+        logging.error("compiled the custom scripts failed. please check it in /data/books/settings/auto.py"
+                      " then restart the container.")
+    return ret
+
+
+def upgrade_new_container():
+    if not CONF["installed"]:
+        return 0
+
+    logging.info("upgrade the container now")
+    # 首先检查是否有未编译的nuxt文件,先进行编译一次。
+    new_container = False
+    try:
+        _ = os.stat(new_container_lock)
+    except FileNotFoundError:
+        new_container = True
+
+    need_compile = True
+    need_fresh_settings = not CONF["scripts_compiled"] or new_container
+
+    if new_container:
+        # 新容器,未配置自定义脚本,则不需要编译。
+        if len(CONF["custom_scripts"]) == 0:
+            need_compile = False
+    else:
+        # 老容器,配置过自定义脚本,但是上次编译成功。
+        if CONF["scripts_compiled"]:
+            need_compile = False
+
+    logging.info("before upgrading: new: %s, has: %s, compiled: %s" %
+                 (new_container, len(CONF["custom_scripts"]), CONF["scripts_compiled"]))
+    if need_compile:
+        logging.info("need compile the custom scripts now")
+        ret = compile_custom_scriots()
+        if ret != 0:
+            return ret
+
+    if need_fresh_settings:
+        logging.info("need refresh the settings now")
+        ret = fresh_settings(CONF)
+        if ret != 0:
+            return ret
+
+    if new_container:
+        # 触发一次空白配置更新
+        logging.info("updating configs ...")
+        from webserver.handlers.admin import SettingsSaverLogic
+        logic = SettingsSaverLogic()
+        logic.update_nuxtjs_env()
+        logging.info("updating configs done ...")
+
+        with open(new_container_lock, "w") as f:
+            f.close()
+        new_container = False
+
+    logging.info("after upgrading: new: %s, has: %s, compiled: %s" %
+                 (new_container, len(CONF["custom_scripts"]), CONF["scripts_compiled"]))
+
+    if need_compile or new_container:
+        logging.info("compiled the scripts, restart the service now")
+        subprocess.call(["supervisorctl", "reload"])
+        return 1
+
+    logging.info("upgrade the container end")
+    return 0
+
+
+def fresh_settings(orig_conf):
+    orig_conf["scripts_compiled"] = True
+    args = loader.SettingsLoader()
+    args.clear()
+    for key, val in orig_conf.items():
+        if key.startswith("SOCIAL_AUTH"):
+            if key.endswith("_KEY") or key.endswith("_SECRET"):
+                args[key] = val
+        elif key in settings.KEYS:
+            args[key] = val
+    args["installed"] = True
+    try:
+        args.dumpfile()
+    except:
+        logging.error(traceback.format_exc())
+        logging.error("更新磁盘配置文件失败!请确保配置文件的权限为可写入!")
+        return 1
+    return 0
+
+
 if __name__ == "__main__":
     sys.path.append(os.path.dirname(__file__))
     sys.exit(main())

+ 2 - 2
webserver/resources/book/read.html

@@ -130,7 +130,7 @@
     });
 
 </script>
-
+<!-- Custom Scripts Start -->
 <!-- Matomo -->
 <script>
   var _paq = window._paq = window._paq || [];
@@ -146,6 +146,6 @@
   })();
 </script>
 <!-- End Matomo Code -->
-
+<!-- Custom Scripts End -->
 </body>
 </html>

+ 132 - 61
webserver/settings.py

@@ -2,43 +2,44 @@
 # -*- coding: UTF-8 -*-
 # fmt: off
 # flake8: noqa
-
+import logging
 import os
 
 settings = {
-    'installed'     : False,
-    "autoreload"    : True,
-    "xsrf_cookies"  : False,
-    "static_host"   : "",
-    "nuxt_env_path" : os.path.join(os.path.dirname(__file__), "../app/.env"),
-    "html_path"     : os.path.join(os.path.dirname(__file__), "../app/dist"),
-    "i18n_path"     : os.path.join(os.path.dirname(__file__), "i18n"),
-    "static_path"   : os.path.join(os.path.dirname(__file__), "resources"),
-    "resource_path" : os.path.join(os.path.dirname(__file__), "resources"),
-    "settings_path" : "/data/books/settings/",
-    "progress_path" : "/data/books/progress/",
-    "convert_path"  : "/data/books/convert/",
-    "upload_path"   : "/data/books/upload/",
-    "scan_upload_path"   : "/data/books/imports/",
-    "extract_path"  : "/data/books/extract/",
-    "with_library"  : "/data/books/library/",
-    "cookie_secret" : "cookie_secret",
-    "cookie_expire" : 7*86400,
-    "login_url"     : "/login",
-    "user_database" : 'sqlite:////data/books/calibre-webserver.db',
-    "site_title"    : u"奇异书屋",
-    "ssl_crt_file"  : "/data/books/ssl/ssl.crt",
-    "ssl_key_file"  : "/data/books/ssl/ssl.key",
-
+    'installed': False,
+    "autoreload": True,
+    "xsrf_cookies": False,
+    "static_host": "",
+    "nuxt_env_path": os.path.join(os.path.dirname(__file__), "../app/.env"),
+    "html_path": os.path.join(os.path.dirname(__file__), "../app/dist"),
+    "i18n_path": os.path.join(os.path.dirname(__file__), "i18n"),
+    "static_path": os.path.join(os.path.dirname(__file__), "resources"),
+    "resource_path": os.path.join(os.path.dirname(__file__), "resources"),
+    "settings_path": "/data/books/settings/",
+    "progress_path": "/data/books/progress/",
+    "convert_path": "/data/books/convert/",
+    "upload_path": "/data/books/upload/",
+    "scan_upload_path": "/data/books/imports/",
+    "extract_path": "/data/books/extract/",
+    "with_library": "/data/books/library/",
+    "cookie_secret": "cookie_secret",
+    "cookie_expire": 7 * 86400,
+    "login_url": "/login",
+    "user_database": 'sqlite:////data/books/calibre-webserver.db',
+    "site_title": u"奇异书屋",
+    "ssl_crt_file": "/data/books/ssl/ssl.crt",
+    "ssl_key_file": "/data/books/ssl/ssl.key",
+    'custom_scripts': '',
+    'scripts_compiled': True,
     # https://analytics.google.com/
-    "google_analytics_id" : "G-LLF01B5ZZ8",
+    "google_analytics_id": "G-LLF01B5ZZ8",
 
-    "opds_will_display"        : ["*"],
-    "opds_wont_display"        : [],
-    "opds_max_tags_shown"      : 10240,
-    "opds_max_items"           : 50,
-    "opds_max_ungrouped_items" : 100,
-    "opds_url_prefix"          : "",
+    "opds_will_display": ["*"],
+    "opds_wont_display": [],
+    "opds_max_tags_shown": 10240,
+    "opds_max_items": 50,
+    "opds_max_ungrouped_items": 100,
+    "opds_url_prefix": "",
 
     "downloads_count_per_ip_limitation": 0,
     "db_engine_args": {
@@ -50,10 +51,10 @@ settings = {
 
     "PDF_VIEWER": "/static/pdfjs/web/viewer.html?file=%(pdf_url)s",
 
-    "SOCIAL_AUTH_LOGIN_URL"          : '/',
-    "SOCIAL_AUTH_LOGIN_REDIRECT_URL" : '/api/done/',
-    "SOCIAL_AUTH_USER_MODEL"         : 'webserver.models.Reader',
-    "SOCIAL_AUTH_AUTHENTICATION_BACKENDS" : (
+    "SOCIAL_AUTH_LOGIN_URL": '/',
+    "SOCIAL_AUTH_LOGIN_REDIRECT_URL": '/api/done/',
+    "SOCIAL_AUTH_USER_MODEL": 'webserver.models.Reader',
+    "SOCIAL_AUTH_AUTHENTICATION_BACKENDS": (
         'social_core.backends.qq.QQOAuth2',
         'social_core.backends.weibo.WeiboOAuth2',
         'social_core.backends.amazon.AmazonOAuth2',
@@ -61,48 +62,48 @@ settings = {
     ),
 
     # See: http://open.weibo.com/developers
-    'SOCIAL_AUTH_WEIBO_KEY'            : '',
-    'SOCIAL_AUTH_WEIBO_SECRET'         : '',
+    'SOCIAL_AUTH_WEIBO_KEY': '',
+    'SOCIAL_AUTH_WEIBO_SECRET': '',
 
     # See: https://connect.qq.com/
-    'SOCIAL_AUTH_QQ_KEY'               : '',
-    'SOCIAL_AUTH_QQ_SECRET'            : '',
+    'SOCIAL_AUTH_QQ_KEY': '',
+    'SOCIAL_AUTH_QQ_SECRET': '',
 
     # See: https://github.com/settings/applications/new
-    'SOCIAL_AUTH_GITHUB_KEY'           : '',
-    'SOCIAL_AUTH_GITHUB_SECRET'        : '',
+    'SOCIAL_AUTH_GITHUB_KEY': '',
+    'SOCIAL_AUTH_GITHUB_SECRET': '',
 
     # See: http://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28
-    'smtp_server'       : "smtp.talebook.org",
-    'smtp_username'     : "sender@talebook.org",
-    'smtp_password'     : "password",
-    'douban_apikey'     : "0df993c66c0c636e29ecbb5344252a4a",
-    'douban_baseurl'    : "https://api.douban.com",
-    'douban_max_count'  : 2,
+    'smtp_server': "smtp.talebook.org",
+    'smtp_username': "sender@talebook.org",
+    'smtp_password': "password",
+    'douban_apikey': "0df993c66c0c636e29ecbb5344252a4a",
+    'douban_baseurl': "https://api.douban.com",
+    'douban_max_count': 2,
 
-    'avatar_service'    : "https://cravatar.cn",
+    'avatar_service': "https://cravatar.cn",
 
     'BOOK_NAMES_FORMAT': 'en',
 
-    'INVITE_MODE'   : False,
-    'INVITE_CODE'   : 'love',
+    'INVITE_MODE': False,
+    'INVITE_CODE': 'love',
     'INVITE_MESSAGE': u'''本站为私人图书馆,需输入密码才可进行访问''',
 
-    'ALLOW_GUEST_READ' : True,
-    'ALLOW_GUEST_PUSH' : True,
-    'ALLOW_GUEST_DOWNLOAD' : True,
+    'ALLOW_GUEST_READ': True,
+    'ALLOW_GUEST_PUSH': True,
+    'ALLOW_GUEST_DOWNLOAD': True,
     'RESTRICT_DOWNLOADS_COUNT_PER_IP': False,
-    'ALLOW_REGISTER' : False,
+    'ALLOW_REGISTER': False,
 
     'FOOTER': '本站基于Calibre构建,感谢开源的力量。所有资源来源于互联网免费资源库,如有侵权请邮件联系。',
 
     'FRIENDS': [
-        { "text": u"芒果读书", "href": "http://diumx.com/" },
-        { "text": u"鸠摩搜索", "href": "https://www.jiumodiary.com/" },
-        { "text": u"文渊阁",   "href": "https://wenyuange.org/" },
-        { "text": u"阅读链",   "href": "https://www.yuedu.pro/" },
-        { "text": u"苦瓜书盘", "href": "https://www.kgbook.com" },
-        { "text": u"三秋书屋", "href": "https://www.sanqiu.cc/" },
+        {"text": u"芒果读书", "href": "http://diumx.com/"},
+        {"text": u"鸠摩搜索", "href": "https://www.jiumodiary.com/"},
+        {"text": u"文渊阁", "href": "https://wenyuange.org/"},
+        {"text": u"阅读链", "href": "https://www.yuedu.pro/"},
+        {"text": u"苦瓜书盘", "href": "https://www.kgbook.com"},
+        {"text": u"三秋书屋", "href": "https://www.sanqiu.cc/"},
     ],
     'SOCIALS': [
     ],
@@ -123,3 +124,73 @@ Hi, %(username)s!
 ''',
 
 }
+
+KEYS = [
+    "ALLOW_GUEST_DOWNLOAD",
+    "ALLOW_GUEST_PUSH",
+    "ALLOW_GUEST_READ",
+    "ALLOW_REGISTER",
+    "BOOK_NAMES_FORMAT",
+    "FRIENDS",
+    "FOOTER",
+    "INVITE_CODE",
+    "INVITE_MESSAGE",
+    "INVITE_MODE",
+    "MAX_UPLOAD_SIZE",
+    "RESET_MAIL_CONTENT",
+    "RESET_MAIL_TITLE",
+    "SIGNUP_MAIL_CONTENT",
+    "SIGNUP_MAIL_TITLE",
+    "SOCIALS",
+    "autoreload",
+    "cookie_secret",
+    "scan_upload_path",
+    "RESTRICT_DOWNLOADS_COUNT_PER_IP",
+    "downloads_count_per_ip_limitation",
+    "douban_apikey",
+    "douban_baseurl",
+    "douban_max_count",
+    "site_title",
+    "smtp_password",
+    "smtp_server",
+    "smtp_username",
+    "static_host",
+    "xsrf_cookies",
+    "settings_path",
+    "avatar_service",
+    "google_analytics_id",
+    "custom_scripts",
+    "scripts_compiled",
+]
+
+cust_scripts_start = "<!-- Custom Scripts Start -->"
+cust_scripts_end = "<!-- Custom Scripts End -->"
+
+
+def replace_scripts(html_file, custom_scripts):
+    tmp_file = html_file + ".tmp"
+    with open(tmp_file, "w") as f:
+        curr_file = open(html_file, "rt")
+        if not curr_file:
+            logging.error("html文件打开失败: %s" % html_file)
+            return
+
+        skip = False
+        while True:
+            line = curr_file.readline()
+            if not line:
+                break
+
+            if skip:
+                if cust_scripts_end in line:
+                    skip = False
+                continue
+
+            if cust_scripts_start in line:
+                line = "%s\n%s\n%s\n" % (cust_scripts_start, custom_scripts, cust_scripts_end)
+                skip = True
+            f.write(line)
+        f.close()
+        curr_file.close()
+        os.replace(tmp_file, html_file)
+        logging.info("set custom scripts to %s successfully." % html_file)