diff --git a/.gitignore b/.gitignore
index 77188e44..b620f04a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ tests/rss/*
tests/debug_mail/*
tests/cov_html/*
.idea/
+datastore/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..c3e1322a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 cdhigh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/app.yaml b/app.yaml
index 104c866c..425732f3 100755
--- a/app.yaml
+++ b/app.yaml
@@ -1,4 +1,5 @@
runtime: python38
+service: default
#B1: 384MB/600MHz/manual_scaling
instance_class: B1
@@ -23,21 +24,19 @@ inbound_services:
handlers:
- url: /robots.txt
- static_files: static/robots.txt
- upload: static/robots.txt
+ static_files: application/static/robots.txt
+ upload: application/static/robots.txt
- url: /favicon.ico
- static_files: static/favicon.ico
- upload: static/favicon.ico
+ static_files: application/static/favicon.ico
+ upload: application/static/favicon.ico
mime_type: image/x-icon
- url: /static
- static_dir: static
- application_readable: true
+ static_dir: application/static
- url: /images
- static_dir: images
- application_readable: true
+ static_dir: application/images
- url: /.*
secure: always
diff --git a/application/__init__.py b/application/__init__.py
index 8ace4219..a474535e 100644
--- a/application/__init__.py
+++ b/application/__init__.py
@@ -4,7 +4,7 @@
# Author: cdhigh
__Author__ = "cdhigh"
-__Version__ = '3.0.0'
+__Version__ = '3.0.0b'
import os, builtins, datetime
from flask import Flask, render_template, session, request, g
@@ -22,12 +22,8 @@ def init_app(name, debug=False):
app = Flask(name, template_folder=template_folder, static_folder=static_folder)
app.config.from_pyfile(os.path.join(rootDir, 'config.py'))
-
- from .utils import new_secret_key
-
- app.config['SECRET_KEY'] = '12345678' # if debug else new_secret_key()
app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024 #32MB
-
+
from .view import setting
app.config["BABEL_TRANSLATION_DIRECTORIES"] = i18n_folder
babel = Babel(app)
diff --git a/application/back_end/db_models.py b/application/back_end/db_models.py
index 8fd49681..3f0a7b49 100644
--- a/application/back_end/db_models.py
+++ b/application/back_end/db_models.py
@@ -17,11 +17,12 @@ class KeUser(MyBaseModel): # kindleEar User
name = CharField(unique=True)
passwd = CharField()
email = CharField()
+ sender = CharField() #可能等于自己的email,也可能是管理员的email
secret_key = CharField(default='')
kindle_email = CharField(default='')
enable_send = BooleanField(default=False)
send_days = JSONField(default=JSONField.list_default)
- send_time = IntegerField(default=0)
+ send_time = IntegerField(default=6)
timezone = IntegerField(default=0)
expiration_days = IntegerField(default=0) #账号超期设置值,0为永久有效
expires = DateTimeField(null=True) #超过了此日期后账号自动停止推送
@@ -114,7 +115,7 @@ class UserBlob(MyBaseModel):
name = CharField()
user = CharField()
time = DateTimeField(default=datetime.datetime.utcnow)
- data = BlobField(null=True)
+ data = BlobField(null=True, index=False)
#RSS订阅源,包括自定义RSS,上传的recipe,内置在zip里面的builtin_recipe不包括在内
#每个Recipe的字符串表示为:custom:id, upload:id
@@ -125,7 +126,7 @@ class Recipe(MyBaseModel):
isfulltext = BooleanField(default=False) #只有自定义RSS才有意义
type_ = CharField() #'custom','upload'
needs_subscription = BooleanField(default=False) #是否需要登陆网页,只有上传的recipe才有意义
- src = TextField(default='') #保存上传的recipe的unicode字符串表示,已经解码
+ src = TextField(default='', index=False) #保存上传的recipe的unicode字符串表示,已经解码
time = DateTimeField() #源被加入的时间,用于排序
user = CharField() #哪个账号创建的,和nosql一致,保存用户名
language = CharField(default='')
@@ -176,7 +177,7 @@ class DeliverLog(MyBaseModel):
to = CharField()
size = IntegerField(default=0)
time_str = CharField() #每个用户的时区可能不同,为显示方便,创建记录时就生成推送时间字符串
- datetime = DateTimeField(index=True)
+ datetime = DateTimeField()
book = CharField(default='')
status = CharField()
@@ -193,7 +194,7 @@ class SharedRss(MyBaseModel):
language = CharField(default='')
category = CharField(default='')
recipe_url = CharField(default='') #客户端优先使用此字段获取recipe,为什么不用上面的url是要和以前的版本兼容
- src = TextField(default='') #保存分享的recipe的unicode字符串表示,已经解码
+ src = TextField(default='', index=False) #保存分享的recipe的unicode字符串表示,已经解码
description = CharField(default='')
creator = CharField(default='') #保存贡献者的md5
created_time = DateTimeField(default=datetime.datetime.utcnow)
@@ -246,4 +247,4 @@ def create_database_tables():
SharedRss, SharedRssCategory, LastDelivered, AppInfo], safe=True)
#close_database()
- #print(f'Create database "{dbName}" finished')
+ return 'Created database tables successfully'
diff --git a/application/back_end/db_models_nosql.py b/application/back_end/db_models_nosql.py
index b1b041ea..fdd7469e 100644
--- a/application/back_end/db_models_nosql.py
+++ b/application/back_end/db_models_nosql.py
@@ -7,7 +7,7 @@
from weedata import *
dbUrl = os.getenv('DATABASE_URL')
-appId = os.getenv('APP_ID', 'kindleear')
+appId = os.getenv('APP_ID')
if dbUrl.startswith('mongodb://'):
dbInstance = MongoDbClient(appId, dbUrl)
diff --git a/application/back_end/send_mail_adpt.py b/application/back_end/send_mail_adpt.py
index ad1c5401..21d52341 100644
--- a/application/back_end/send_mail_adpt.py
+++ b/application/back_end/send_mail_adpt.py
@@ -5,16 +5,21 @@
#gae mail api
#https://cloud.google.com/appengine/docs/standard/python3/reference/services/bundled/google/appengine/api/mail
#https://cloud.google.com/appengine/docs/standard/python3/services/mail
-import os, datetime, zipfile
+import os, datetime, zipfile, base64
from ..utils import local_time, ke_decrypt
from ..base_handler import save_delivery_log
from .db_models import KeUser
-try:
- from google.appengine.api import mail as gae_mail
- from google.appengine.api.mail_errors import InvalidSenderError, InvalidAttachmentTypeError, InvalidEmailError
- from google.appengine.runtime.apiproxy_errors import OverQuotaError, DeadlineExceededError
-except ImportError:
+#google.appengine will apply patch for os.env module
+hideMailLocal = os.getenv('HIDE_MAIL_TO_LOCAL')
+
+#判断是否是部署在gae平台
+if os.getenv('DATABASE_URL') == 'datastore':
+ try:
+ from google.appengine.api import mail as gae_mail
+ except ImportError:
+ gae_mail = None
+else:
gae_mail = None
try:
@@ -23,6 +28,11 @@
except ImportError:
SendGridAPIClient = None
+try:
+ from mailjet_rest import Client as MailjetClient
+except ImportError:
+ MailjetClient = None
+
try:
from smtp_mail import smtp_send_mail
except ImportError:
@@ -35,9 +45,11 @@ def avaliable_sm_services():
sm.append('gae')
if SendGridAPIClient:
sm.append('sendgrid')
+ if MailjetClient:
+ sm.append('mailjet')
if smtp_send_mail:
sm.append('smtp')
- if not os.getenv('HIDE_MAIL_TO_LOCAL'):
+ if not hideMailLocal:
sm.append('local')
return sm
@@ -53,27 +65,28 @@ def send_to_kindle(user, title, attachment, fileWithTime=True):
lcTime = "({})".format(lcTime) if fileWithTime else ""
fileName = f"{title}{lcTime}.{user.book_type}"
attachment = (fileName, attachment)
-
- if not isinstance(attachment, list):
- attachments = [attachment]
+ if not isinstance(attachment, list):
+ attachment = [attachment]
+
status = 'ok'
body = "Deliver from KindleEar"
try:
- send_mail(user, user.kindle_email, subject, body, attachments)
+ send_mail(user, user.kindle_email, subject, body, attachment)
except Exception as e:
status = str(e)
default_log.warning(f'Failed to send mail "{title}": {status}')
- save_delivery_log(user, title, len(attachment), status=status)
+ size = sum([len(a[1]) for a in attachment])
+ save_delivery_log(user, title, size, status=status)
#统一的发送邮件函数
def send_mail(user, to, subject, body, attachments=None, html=None):
- if not isinstance(to, list) and (',' in to):
+ if not isinstance(to, list):
to = to.split(',')
sm_service = user.get_send_mail_service()
- srv_type = sm_service.get('service', 'gae')
- data = {'sender': os.getenv('SRC_EMAIL'), 'to': to, 'subject': subject, 'body': body}
+ srv_type = sm_service.get('service', '')
+ data = {'sender': user.sender, 'to': to, 'subject': subject, 'body': body}
if attachments:
data['attachments'] = attachments
if html:
@@ -85,10 +98,14 @@ def send_mail(user, to, subject, body, attachments=None, html=None):
elif srv_type == 'sendgrid':
apikey = sm_service.get('apikey', '')
grid_send_mail(apikey=apikey, **data)
+ elif srv_type == 'mailjet':
+ apikey = sm_service.get('apikey', '')
+ secret_key = sm_service.get('secret_key', '')
+ mailjet_send_mail(apikey=apikey, secret_key=secret_key, **data)
elif srv_type == 'smtp':
data['host'] = sm_service.get('host', '')
data['port'] = sm_service.get('port', 587)
- data['username'] = sm_service.get('username', '')
+ data['username'] = user.sender
data['password'] = ke_decrypt(sm_service.get('password', ''), user.secret_key)
smtp_send_mail(**data)
elif srv_type == 'local':
@@ -98,7 +115,7 @@ def send_mail(user, to, subject, body, attachments=None, html=None):
#发送一个HTML邮件
#user: KeUser实例
-#to: 收件地址,可以是一个单独的字符串,或一个字符串列表,对应发送到多个地址
+#to: 收件地址列表
#subject: 邮件标题
#html: 邮件正文的HTML内容
#attachments: 附件文件名和二进制内容,[(fileName, content),...]
@@ -120,12 +137,11 @@ def send_html_mail(user, to, subject, html, attachments=None, body=None):
#SendGrid发送邮件
#sender:: 发送者地址
-#to: 收件地址,可以是一个单独的字符串,或一个字符串列表,对应发送到多个地址
+#to: 收件地址列表
#subject: 邮件标题
#body: 邮件正文纯文本内容
#html: 邮件正文HTML
#attachment: [(fileName, attachment),]
-#tz: 时区
def grid_send_mail(apikey, sender, to, subject, body, html=None, attachments=None):
global default_log
sgClient = SendGridAPIClient(apikey)
@@ -146,6 +162,43 @@ def grid_send_mail(apikey, sender, to, subject, body, html=None, attachments=Non
response = sgClient.send(message)
if response.status_code not in (200, 202):
raise Exception(f'sendgrid failed: {response.status_code}')
+
+#Mailjet发送邮件
+#sender:: 发送者地址
+#to: 收件地址列表
+#subject: 邮件标题
+#body: 邮件正文纯文本内容
+#html: 邮件正文HTML
+#attachment: [(fileName, attachment),]
+def mailjet_send_mail(apikey, secret_key, sender, to, subject, body, html=None, attachments=None):
+ global default_log
+ mjClient = MailjetClient(auth=(apikey, secret_key), version='v3.1')
+ to = [{'Email': t, 'Name': t} for t in to]
+ data = {'Messages': [{
+ "From": {"Email": sender, "Name": sender},
+ "To": to,
+ "Subject": subject,
+ "TextPart": body,
+ }],}
+
+ dataDict = data['Messages'][0]
+ if html:
+ dataDict['HTMLPart'] = html
+ if attachments:
+ dataDict['Attachments'] = []
+ for fileName, content in (attachments or []):
+ dataDict['Attachments'].append({"ContentType": "text/plain", "Filename": fileName,
+ "Base64Content": base64.b64encode(content).decode()})
+
+
+ resp = mjClient.send.create(data=data)
+ if resp.status_code in (200, 202):
+ status = resp.json()["Messages"][0]["Status"]
+ print(resp.json()) #TODO
+ if status != "success":
+ raise Exception(f'mailjet failed: {status}')
+ else:
+ raise Exception(f'mailjet failed: {resp.status_code}')
def save_mail_to_local(dest_dir, subject, body, attachments=None, html=None, **kwargs):
attachments = attachments or []
diff --git a/application/back_end/task_queue_apscheduler.py b/application/back_end/task_queue_apscheduler.py
index 022e3b48..63f750de 100644
--- a/application/back_end/task_queue_apscheduler.py
+++ b/application/back_end/task_queue_apscheduler.py
@@ -23,7 +23,7 @@
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
jobstores = {"default": SQLAlchemyJobStore(url=_broker_url)}
elif _broker_url == '':
- jobstores = None #default is memory store
+ jobstores = {} #default is memory store
else:
raise ValueError('Unsupported TASK_QUEUE_BROKER_URL type: {_broker_url}')
diff --git a/application/back_end/task_queue_gae.py b/application/back_end/task_queue_gae.py
index 2958fa0a..10bde17b 100644
--- a/application/back_end/task_queue_gae.py
+++ b/application/back_end/task_queue_gae.py
@@ -1,10 +1,15 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-#任务队列GAE
+#任务队列GAE接口实现
#Author: cdhigh
-import json
+import os, json
+from urllib.parse import urlencode, urlparse
+appId = os.getenv('APP_ID')
+serverLoc = os.getenv('SERVER_LOCATION')
+
from google.cloud import tasks_v2
DEFAULT_QUEUE_NAME = "default"
+TASK_HTTP_METHOD = tasks_v2.HttpMethod.GET
def init_task_queue_service(app):
pass
@@ -19,22 +24,18 @@ def create_url2book_task(payload: dict):
#创建一个任务
#url: 任务要调用的链接
#payload: 要传递给url的参数,为一个Python字典
+#之所以使用HTTP.GET但是这里传入的是payload,而不是在调用方将payload合入url是为了兼容各种任务队列实现
#返回创建的任务实例
def create_http_task(url, payload):
client = tasks_v2.CloudTasksClient()
+ taskParent = client.queue_path(appId, serverLoc, DEFAULT_QUEUE_NAME)
- task = {"app_engine_http_request": {
- "http_method": tasks_v2.HttpMethod.GET,
- "relative_uri": url,}
- }
- if payload:
- task["app_engine_http_request"]["headers"] = {"Content-type": "application/json"}
- task["app_engine_http_request"]["body"] = json.dumps(payload).encode()
- return client.create_task(task=task)
+ task = {"app_engine_http_request": {"http_method": TASK_HTTP_METHOD}}
- #httpRequest = tasks_v2.HttpRequest(http_method=tasks_v2.HttpMethod.GET, url=url,
- # headers={"Content-type": "application/json"}, body=json.dumps(payload).encode(),)
- #task = tasks_v2.Task(httpRequest=httpRequest)
- #taskParent = client.queue_path(APP_ID, SERVER_LOCATION, DEFAULT_QUEUE_NAME)
- #return client.create_task(tasks_v2.CreateTaskRequest(parent=taskParent, task=task))
-
+ if TASK_HTTP_METHOD == tasks_v2.HttpMethod.GET: #转换字典为查询字符串
+ params = {'relative_uri': urlparse(url)._replace(query=urlencode(payload, doseq=True)).geturl()}
+ else: #转换字典为post的body内容
+ params = {'relative_uri': url, 'headers': {"Content-type": "application/json"}, 'body': json.dumps(payload).encode()}
+
+ task["app_engine_http_request"].update(params)
+ return client.create_task(request={'parent': taskParent, 'task': task})
diff --git a/application/base_handler.py b/application/base_handler.py
index 34854bbc..c077ccdd 100644
--- a/application/base_handler.py
+++ b/application/base_handler.py
@@ -17,7 +17,7 @@ def login_required(forAjax=False):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
- if ((session.get('login') == 1) and get_login_user()):
+ if (session.get('login', '') == 1) and session.get('userName', ''):
return func(*args, **kwargs)
else:
return redirect(url_for("bpLogin.NeedLoginAjax") if forAjax else url_for("bpLogin.Login"))
@@ -27,7 +27,8 @@ def wrapper(*args, **kwargs):
#查询当前登录用户名,在使用此函数前最好保证已经登录
#返回一个数据库行实例,而不是一个字符串
def get_login_user():
- return KeUser.get_or_none(KeUser.name == session.get('userName', ''))
+ name = session.get('userName', '')
+ return KeUser.get_or_none(KeUser.name == name) if name else None
#记录投递记录到数据库
def save_delivery_log(user, book, size, status='ok', to=None):
diff --git a/application/lib/build_ebook.py b/application/lib/build_ebook.py
index fd02abbd..d935b10d 100644
--- a/application/lib/build_ebook.py
+++ b/application/lib/build_ebook.py
@@ -68,8 +68,8 @@ def ke_opts(user, options=None):
options.setdefault('dont_split_on_page_breaks', True)
options['user'] = user
- options.setdefault('debug_pipeline', os.getenv('TEMP_DIR'))
- options.setdefault('verbose', 1)
- options.setdefault('test', 1)
+ #options.setdefault('debug_pipeline', os.getenv('TEMP_DIR'))
+ #options.setdefault('verbose', 1)
+ #options.setdefault('test', 1)
return options
diff --git a/application/lib/calibre/web/feeds/news.py b/application/lib/calibre/web/feeds/news.py
index aefbb444..26bbb599 100644
--- a/application/lib/calibre/web/feeds/news.py
+++ b/application/lib/calibre/web/feeds/news.py
@@ -1099,7 +1099,7 @@ def append_share_links(self, soup, url):
ashare.string = _('Save to {}').format(type_)
aTags.append(ashare)
- for type_ in ['Weibo', 'TencentWeibo', 'Facebook', 'X', 'Tumblr']:
+ for type_ in ['Weibo', 'Facebook', 'X', 'Tumblr']:
if shareLinks.get(type_):
ashare = soup.new_tag('a', href=self.make_share_link(type_, user, url, soup))
ashare.string = _('Share on {}').format(type_)
@@ -1127,17 +1127,15 @@ def make_share_link(self, shareType, user, url, soup):
if shareType in ('Evernote', 'Wiz'):
href = f"{appDomain}/share?act={shareType}&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}"
elif shareType == 'Pocket':
- href = f'{appDomain}/share?act=pocket&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}'
+ href = f'{appDomain}/share?act=Pocket&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}'
elif shareType == 'Instapaper':
- href = f'{appDomain}/share?act=instapaper&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}'
+ href = f'{appDomain}/share?act=Instapaper&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}'
elif shareType == 'Weibo':
href = f'https://service.weibo.com/share/share.php?url={quote_plus(url)}'
- elif shareType == 'TencentWeibo':
- href = f'https://share.v.t.qq.com/index.php?c=share&a=index&url={quote_plus(url)}'
elif shareType == 'Facebook':
href = f'https://www.facebook.com/share.php?u={quote_plus(url)}'
elif shareType == 'X':
- href = f'https://twitter.com/home?status={quote_plus(url)}'
+ href = f'https://twitter.com/intent/post?text={title}&url={quote_plus(url)}'
elif shareType == 'Tumblr':
href = f'https://www.tumblr.com/share/link?url={quote_plus(url)}'
else:
@@ -1153,7 +1151,7 @@ def download(self):
Calling it more than once will lead to undefined behavior.
:return: Path to index.html
'''
- if 1:
+ try:
res = self.build_index()
if self.failed_downloads:
self.log.warning(_('Failed to download the following articles:'))
@@ -1171,8 +1169,8 @@ def download(self):
self.log.warning(l)
self.log.debug(tb)
return res
- #finally:
- # self.cleanup()
+ finally:
+ self.cleanup()
@property
def lang_for_html(self):
diff --git a/application/lib/calibre/web/fetch/simple.py b/application/lib/calibre/web/fetch/simple.py
index fddbdcf3..66ff58e9 100644
--- a/application/lib/calibre/web/fetch/simple.py
+++ b/application/lib/calibre/web/fetch/simple.py
@@ -452,17 +452,17 @@ def process_images(self, soup, baseurl):
tag['src'] = imgpath
else:
from calibre.utils.img import image_from_data, image_to_data
- if 1:
+ try:
# Ensure image is valid
img = image_from_data(data)
if itype not in {'png', 'jpg', 'jpeg'}:
itype = 'png' if itype == 'gif' else 'jpeg'
data = image_to_data(img, fmt=itype)
if self.compress_news_images and itype in {'jpg','jpeg'}:
- if 1:
+ try:
data = self.rescale_image(data)
- #except Exception:
- # self.log.exception('failed to compress image '+iurl)
+ except Exception:
+ self.log.exception('failed to compress image '+iurl)
# Moon+ apparently cannot handle .jpeg files
if itype == 'jpeg':
itype = 'jpg'
@@ -471,9 +471,9 @@ def process_images(self, soup, baseurl):
self.imagemap[iurl] = imgpath
self.fs.write(imgpath, data, 'wb')
tag['src'] = imgpath
- #except Exception:
- #traceback.print_exc()
- #continue
+ except Exception:
+ traceback.print_exc()
+ continue
#如果需要,纠正或规则化soup里面的图片地址,比如延迟加载等
def rectify_image_src(self, soup, baseurl=None):
@@ -546,7 +546,7 @@ def process_links(self, soup, baseurl, recursion_level, into_dir='links'):
self.fs.mkdir(diskpath)
prev_dir = self.current_dir
- if 1:
+ try:
self.current_dir = diskpath
tags = list(soup.find_all('a', href=True))
@@ -632,7 +632,7 @@ def process_links(self, soup, baseurl, recursion_level, into_dir='links'):
finally:
self.current_dir = diskpath
self.files += 1
- #finally:
+ finally:
self.current_dir = prev_dir
if self.show_progress:
print()
diff --git a/application/lib/pocket.py b/application/lib/pocket.py
index 76113a55..c945601b 100644
--- a/application/lib/pocket.py
+++ b/application/lib/pocket.py
@@ -60,7 +60,7 @@ def __str__(self):
class Pocket(object):
REQUEST_TOKEN_URL = 'https://getpocket.com/v3/oauth/request'
- AUTH_TOKEN_URL = 'https://getpocket.com/auth/authorize?request_token=%(request_token)s&redirect_uri=%(redirect_uri)s'
+ AUTH_TOKEN_URL = 'https://getpocket.com/auth/authorize?request_token={}&redirect_uri={}'
ACCESS_TOKEN_URL = 'https://getpocket.com/v3/oauth/authorize'
POCKET_HEADERS = {
@@ -76,9 +76,13 @@ def __init__(self, consumer_key, redirect_uri=None):
def _post(self, method_url, **kw):
ret = self.opener.open(method_url, data=kw)
- if ret.status_code != 200:
+ if ret.status_code > 399:
raise APIError(ret.status_code, ret.headers.get('X-Error-Code',''), ret.headers.get('X-Error',''), 'Get access token')
- return json.loads(ret.content)
+
+ try:
+ return json.loads(ret.content)
+ except:
+ return json.text
def _authenticated_post(self, method_url, **kw):
kw['consumer_key'] = self.consumer_key
@@ -88,19 +92,18 @@ def _authenticated_post(self, method_url, **kw):
def get_request_token(self):
#此步仅用来直接通过一次http获取一个request_token(code),pocket不会回调redirect_uri
ret = self._post(self.REQUEST_TOKEN_URL, consumer_key=self.consumer_key, redirect_uri=self.redirect_uri)
- return ret.get('code', '')
+ return ret.get('code', '') if isinstance(ret, dict) else ret.split('=')[-1]
def get_authorize_url(self, code):
if not self.redirect_uri:
raise APIError(400, '140', 'Missing redirect url.', 'Get access token')
- url = self.AUTH_TOKEN_URL % {'request_token' : code, 'redirect_uri' : self.redirect_uri}
- return url
+ return self.AUTH_TOKEN_URL.format(code, self.redirect_uri)
def get_access_token(self, code):
# access token : {"access_token":"dcba4321-dcba-4321-dcba-4321dc","username":"pocketuser"}.
ret = self._post(self.ACCESS_TOKEN_URL, consumer_key=self.consumer_key, code=code)
self.access_token = ret.get('access_token', '')
- return ret
+ return self.access_token
def set_access_token(self, access_token):
self.access_token = str(access_token)
diff --git a/application/lib/recipe_helper.py b/application/lib/recipe_helper.py
index 22cd18f2..deed9bf7 100644
--- a/application/lib/recipe_helper.py
+++ b/application/lib/recipe_helper.py
@@ -49,9 +49,10 @@ class {classname}({base}):
#能使用点号访问的字典
class DotDict(dict):
+ #__setattr__ = dict.__setitem__
+ #__getattr__ = dict.__getitem__
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
-
def __getattr__(self, key):
try:
return self[key]
diff --git a/application/lib/smtp_mail.py b/application/lib/smtp_mail.py
index f0cf9e91..0979ff23 100644
--- a/application/lib/smtp_mail.py
+++ b/application/lib/smtp_mail.py
@@ -32,10 +32,12 @@ def smtp_send_mail(sender, to, subject, body, host, username, password, port=Non
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
message.attach(part)
- with smtplib.SMTP(host=host, port=port) as smtp_server:
+ klass = smtplib.SMTP if port != 465 else smtplib.SMTP_SSL
+ with klass(host=host, port=port) as smtp_server:
smtp_server.connect(host, port)
smtp_server.ehlo()
smtp_server.starttls()
smtp_server.ehlo()
smtp_server.login(user=username, password=password)
smtp_server.sendmail(sender, to, message.as_string())
+
diff --git a/application/static/base.css b/application/static/base.css
index c720311e..71dcb1d2 100644
--- a/application/static/base.css
+++ b/application/static/base.css
@@ -572,7 +572,7 @@ table.pure-table thead {
color: #fff;
text-align: center;
padding: 3px 15px;
- white-space: nowrap;
+ /*white-space: nowrap;*/
}
.logs .status.success {
@@ -880,3 +880,44 @@ div[class="schedule_daytimes"] input {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
z-index: 9999;
}
+
+#up_cover_progress {
+ width:300px;
+ height:25px;
+ border:1px solid #98AFB7;
+ border-radius:5px;
+ margin:0 auto;
+}
+#up_cover_progress_bar {
+ width:0px;
+ height:25px;
+ border-radius:5px;
+ background:#5EC4EA;
+ text-align: center;
+}
+
+.tooltip {
+ position: relative;
+ display: inline-block;
+ border-bottom: 1px dotted black;
+ cursor: help;
+}
+.tooltip .tooltiptext {
+ visibility: hidden;
+ width: 500px;
+ background-color: #555;
+ color: #fff;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 10px;
+ position: absolute;
+ z-index: 1;
+ top: calc(-100%); /*calc(10% + 5px);*/
+ left: calc(50% - 500px);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+.tooltip:hover .tooltiptext {
+ visibility: visible;
+ opacity: 1;
+}
\ No newline at end of file
diff --git a/application/static/base.js b/application/static/base.js
index c08350dd..662afc5e 100644
--- a/application/static/base.js
+++ b/application/static/base.js
@@ -1061,7 +1061,8 @@ function startUploadCoversToServer(url) {
alert(i18n.imgSizeToLarge);
return;
}
-
+
+ $("#up_cover_progress").show();
$.ajax({
type: "post",
url: url,
@@ -1069,7 +1070,21 @@ function startUploadCoversToServer(url) {
cache: false,
contentType: false,
processData: false,
+ xhr: function() {
+ var xhr = new window.XMLHttpRequest();
+ xhr.upload.addEventListener("progress", function(evt) {
+ if (evt.lengthComputable) {
+ var percent = Math.round((evt.loaded / evt.total) * 100);
+ $("#up_cover_progress_bar").css("width", String(percent) + "%");
+ $("#up_cover_progress_bar").html(String(percent) + "%");
+ }
+ }, false);
+ return xhr;
+ },
success: function(resp, status, xhr) {
+ $("#up_cover_progress").hide();
+ $("#up_cover_progress_bar").css("width", "0px");
+ $("#up_cover_progress_bar").html('');
if (resp.status == "ok") {
ShowSimpleModalDialog('' + i18n.uploadCoversOk + '
');
} else {
@@ -1077,6 +1092,9 @@ function startUploadCoversToServer(url) {
}
},
error: function(xhr, status, error) {
+ $("#up_cover_progress").hide();
+ $("#up_cover_progress_bar").css("width", "0px");
+ $("#up_cover_progress_bar").html('');
alert(status);
}
});
diff --git a/application/static/faq.html b/application/static/faq.html
deleted file mode 100644
index b2112995..00000000
--- a/application/static/faq.html
+++ /dev/null
@@ -1,448 +0,0 @@
-
-
-
-
- FAQ - KindleEar
-
-
-
-
-
-
-
-
-
- KindleEar是什么?
- KindleEar是开源免费网络应用,可以部署在大多数支持Python的托管平台,包括但不限于google appengine,Heroku,Pythonanywhere,主要功能是自动定期通过RSS收集网络文章然后制作成图文并茂的电子书推送至你的Kindle。
- 主要特性:
-
- 自定义RSS
- 支持RSS全文抓取
- 多账号管理,也就是支持多kindle
- 带图的杂志格式MOBI
- 自动每天定时推送
- 内置共享库,可以直接订阅其他网友分享的订阅源,也可以分享自己的订阅源给其他网友
- 强大并且方便的邮件中转服务
- 和Evernote/Pocket/Instapaper等系统的集成
-
- 优点:
-
- 全免费,不单此应用免费,而是GAE免费提供托管空间,你不需要付一分钱。
- 生成精美的杂志格式书籍。样例如下(左一为kindle3,右两个为kindle touch):
-
-
-
-
-
- 此应用需要自己的服务器吗?
- 此应用托管在Google App Engine(GAE)服务器上,不需要你自己的服务器。
- 注意:GAE(Google App Engine)不是GCE(Google Computer Engine),只要应用使用的资源不超其免费额度,则GAE永久免费,但是GCE仅提供60天试用免费。
-
-
-
- 如何搭建自己的推送服务器?
-
-
- 在 Github 页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如C:\KindleEar(下面以这个为例)。
- 首先你需要一个google账号,并暂时 [启用不够安全的应用的访问权限] 以便获得上传程序的权限。然后访问 Google Developers Console ,新建一个Application。(用来上传KinldeEar用)。
- 安装 Python 2.7.x ,在此页面中选择你的系统平台对应的2.7.x版本下载,然后直接双击安装。注意不要安装成Python 3.x版本,KindleEar都暂不支持。
- 下载 GAE SDK ,选择273之前的版本,比如下面几个链接。
-
- google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip
- google-cloud-sdk-273.0.0-darwin-x86.tar.gz
- google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz
- google-cloud-sdk-273.0.0-linux-x86.tar.gz
- google-cloud-sdk-273.0.0-linux-x86_64.tar.gz
- google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip
- google-cloud-sdk-273.0.0-windows-x86.zip
- google-cloud-sdk-273.0.0-windows-x86_64.zip
-
-
- 在app.yaml的同一个目录,打开config.py文件,修改SRC_EMAIL为你的gmail账号,DOMAIN为你的应用的域名。其他的选项也可以根据注释有选择性的修改,或者使用默认即可。
- 正式上传KindleEar。
-
- 依次点击:开始-运行-输入CMD,打开命令行窗口,进入KindleEar目录
- gcloud auth login
- gcloud config set project 你的ApplicationId
- gcloud app deploy --version=1 app.yaml module-worker.yaml
- gcloud app deploy --version=1 KindleEar目录
- 如果出现错误,可能需要下面几条命令。
-
- gcloud datastore indexes create index.yaml
- gcloud app deploy --version=1 app.yaml queue.yaml
- gcloud app deploy --version=1 app.yaml cron.yaml
- gcloud app deploy --version=1 app.yaml dispatch.yaml
-
-
-
-
- 显示上传成功后使用你的浏览器打开域名:kindleear123.appspot.com (举例而已,kindleear123要改为你申请的GAE程序名称),如果能正常打开,则还有最后一步。
- 将你的gmail账号加入kindle推送邮箱的白名单。这一步你应该早就会了吧。
- 然后呢?享受你自己的推送服务吧!
-
-
-
-
- 为什么我收不到推送的RSS?
- 收不到推送的RSS原因很多,如下是几种可能:
-
- 部署时仅部署了default模块,而没有部署worker模块。比如使用Launcher默认配置上传则导致此情况。
- 没有打开"自定义RSS"的投递开关,此开关在设置网页的最下面。
- 没有在kindle邮箱的白名单中加入你的gmail地址:
-
- 添加RSS过多导致内存占用过大,程序被自动终止。
- 打开module-worker.yaml,将instance_class值修改为B4,然后重新上传。
-
- 你输入的订阅地址不是合法的RSS/ATOM订阅(XML格式)。
- 如果RSS订阅源是Feedburner生成的,你可以在其链接之后添加一个查询字符串'?format=xml'或'?fmt=xml',比如:http://xx.com/feed?format=xml.
-
-
-
- 何为全文RSS?
- 全文RSS就是在RSS的XML文件中已经给出了文章全文的RSS,使用浏览器打开RSS对应的链接,查看是否已经有全部的文章内容?如果是,则为全文RSS。如果仅给出文章摘要,则不是。
-
-
- 全文RSS能否按照摘要RSS处理?反之是否可以?
- 全文RSS当然可以按照摘要RSS处理,这样就忽略RSS链接中给出的文章内容,而直接到原链接中获取,只是时间要多花费不少,导致支持的RSS数量下降。如果是摘要RSS,则不能按全文RSS处理,否则会导致文章内容不全。
-
-
- 如何按周订阅?
- 有些RSS每周更新,这样每天推送同样的内容就蛋疼了点,碰到这种情况,有两种处理方法:
-
- 在books目录下添加一个py文件,将参数deliver_days设置为你需要推送的星期(头字母大写的英文星期单词),注意deliver_days是一个列表,你也可以设置为几个单词,这样的话,其实也可以设置为哪天不推送。
- 比如:deliver_days=['Friday']
deliver_days=['Sunday', 'Friday']
- 使用管理员登陆,新增一个账号,退出,使用新账号登陆,添加需要的RSS,然后在“设置”页面仅选择你需要的其中一个“投递日”,在页面下方的“最旧文章”中选择“一个星期”。
-
-
- 如何一天推送多次?
- 需要及时跟踪网站最新消息的同学可能有一天推送多次的需求,以下步骤告诉你如何实现:
-
- 在books目录下添加一个py文件。
- deliver_times设置为你需要推送的整点时间列表。
- 比如:deliver_times=[6,14,22] #6:00,14:00,22:00三次推送
-
- 如果需要,oldest_article设置为多次推送的间隔秒数,这样就不推送重复文章了。
- 比如:oldest_article=28800 #8*60*60
-
-
-
-
-
- URL过滤器功能如何使用?
- URL过滤器为高级功能,面向懂python正则表达式的同学,应用场景是这样的:有一些RSS的一部分固定的图像会一直下载不了或者其本身就是烦人的广告图像,为了节省时间支持更多的RSS或更好的阅读体验,则可以过滤这些图像URL,不再下载。URL过滤器支持正则表达式,请严格按python正则表达式的语法编写URL过滤器。
-
-
- 部分RSS有敏感内容,被墙屏蔽了怎么办?
- 升级到1.5及以上版本,并且将被qiang的RSS的链接协议改为https。
-
-
- 自定义书籍py文件,如何将封面/报头放到py同一目录?
- 升级到1.5及以上版本,在封面/报头文件设置中有全路径信息即可,如下:
- mastheadfile = 'books/xxxx.gif'
- coverfile = 'books/xxxx.jpg'
-
-
- GAE上传遇到SSL出错,如何解决?
- 墙内的朋友有时候在上传代码时遇到此错误:urllib2.URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in violation of protocol>,如果这样,改hosts即可:“74.125.229.174 appengine.google.com”
-
-
- 有部分文章乱码?
- 对于自定义RSS和不指定编码的内置书籍,软件使用多种方法综合确定网页的编码:HTTP头/HTML头/python模块chardet检测的编码,最终结果并不是100%正确的,如果检测错误,则文章可能乱码,这在浏览器上偶尔碰到网页乱码一样道理。
- 因为这种情况很少见,KindleEar仅提供一个不是很优雅的hack方案:手工更改数据库缓存的chardet检测的编码。 方法如下:
-
使用admin登陆appid.appspot.com(假定你的应用名字为appid)。
- 浏览器地址栏中输入https://appid.appspot.com/dbviewer,翻到页面UrlEncoding区段,查看乱码文章对应网站的编码信息,如果编码不对,记下ID号,进行下一步。
- 地址栏中输入https://appid.appspot.com/dbviewer?action=modurlenc&id=dbid&feedenc=fenc&pageenc=penc
- 解释一下,在这一长串字符串中,你先要更改:
- dbid:你记下来的编码对应网站ID。
- fenc:你希望更改FeedEncoding的编码。
- penc:你希望更改PageEncoding的编码。
-
- 或者你可以在GAE后台的Datastore Viewer中修改。
- 至于怎么获取正确的编码,可以通过在浏览器中查看网页源码。
- 如果你在dbviewer页面中找不到你需要修改的网站,则说明KindleEar一直没有使用chardet检测过此网站任何网页编码,仅使用HTTP头/HTML头信息,这种情况你可以设置config.py文件中的ALWAYS_CHAR_DETECT变量为True再尝试。
- 如果设置ALWAYS_CHAR_DETECT为True还是乱码,则说明chardet检测到的编码错误,并且HTTP头/HTML头编码信息不一致(还是有这种奇葩的网站的),这种情况还可以尝试将TRUST_ENCODING_IN_HEADER_OR_META设置为True。
- 尝试了ALWAYS_CHAR_DETECT和TRUST_ENCODING_IN_HEADER_OR_META都不行的话,软件无能为力了,请增加一个py文件,然后手工指定正确的编码吧。
-
-
-
-
- 忘记密码了怎么办?
- 如果是忘记非管理员账号的密码,则可以通知管理员改密码。 如果是管理员密码,则可以登陆 App Engine ,在左边选择 "Datastore | 实体",在种类里面选择 "KeUser",选中 name 为 admin 的行,记下 "secret_key" 的值,比如为 "abcdefgh",在前面加随便几个字符,比如 "123abcdefgh",然后随便搜索一个在线的MD5计算网站,将其转换为MD5摘要字符串,比如 "8b920d97ca5fceff6c5835223c541bf4",再点击修改上面选择的数据行,将最后的MD5字符串填写到passwd域,然后点击页面下方的保存,重新使用密码 "123" 登录。
-
-
-
xxx@appid.appspotmail.com邮件地址怎么用?
- xxx@appid.appspotmail.com是KindleEar附送的邮件服务,在部署好KindleEar后,你自动拥有了无数个EMAIL邮箱地址,格式为:xxx@appid.appspotmail.com,xxx为任意合法字符串,appid为你的应用名。
-
- 要使用此功能,先要添加白名单,如果为 '*' 则允许所有邮件,否则格式为 'xx@xx.xx' 或 '@xx.xx' (不包括单引号)。
- 此邮箱将收到的邮件正文转换为邮件附件推送至你注册的Email邮箱,。如果邮件中只有链接(多个链接则每行一个),则抓取链接的网页内容制作成电子书然后再推送。
- 如果在邮件主题最后添加了标识 !links,则不论邮件内容如何,KindleEar都只会提取邮件中的链接,然后抓取网页,制作成电子书发送至你的Kindle。这个功能最适合将网络连载直接发送至Kindle观看。
- 如果在邮件主题后添加了标识 !article,则忽略所有链接,直接将内容转换为电子书发送。
- 默认推送至管理员注册的邮箱,如果要推送至其他用户的邮箱,则使用格式:username__xxx@appid.appspotmail.com。(注意是双下划线)
- 如果将电子书下载链接发送至book@appid.appspotmail.com 或 username__book@appid.appspotmail.com则KindleEar直接下载对应的电子书并转发至注册的邮箱(注意GAE对后缀名有限制,不能发送可能有安全隐患的文件后缀比如exe等,zip文件能发送,但是zip文件内不能包含可能有安全隐患的文件)。
- GAE可邮件发送的后缀名列表参见:Mail Python API Overview
- (book/file/download邮件地址保留为下载电子书使用)
-
- 发送至trigger@appid.appspotmail.com 或 username__trigger@appid.appspotmail.com,则触发一次手动投递。邮件标题为空或为all则完全等同于网页上的“现在投递”按钮。如果需要推送特定书籍,则在标题上写书籍名字,多个书籍名字使用逗号分隔。 注意:邮件标题上写多个书籍名字则自动合并推送为一本书。
- 发送至debug@appid.appspotmail.com的邮件则直接抓取邮件中的链接并直接发送HTML文件至管理员邮箱。
-
-
-
-
- 推送至亚马逊中国邮箱有比较大的延迟?
- 这个经过网友们试验,和伟大的墙有关系,有一个解决方法:KindleEar先推送至某个个人邮箱,然后此个人邮箱自动转发至中亚的kindle邮箱。 不过还面临一个问题:部分邮箱设置自动转发需要发到邮箱的验证,而亚马逊邮箱不能登陆无法验证。其实你需要的是找一个不需要目标邮箱确认的邮箱来设置自动转发,比如outlook.com。
-
-
- 为何合并多本书籍推送后无法查词典?
- 如果有外文RSS而且需要在kindle上查字典的话,请将自定义RSS的语言设置为对应的外文。
- 同时,还要注意合并书籍推送的其他特性:
-
- 合并后的书籍使用默认报头和封面;
- 合并后的书籍使用自定义RSS设定的标题和语言设置;
-
-
-
-
- “高级设置”里的归档和分享功能如何使用?
- 启用此功能后,KindleEar会在每篇文件的最后都附加一个超链接,在Kindle上点击此超链接会自动打开浏览器将你正在阅读的此文章归档至网络笔记或分享至社交网络(需要打开网络连接)。
- 有几点需要注意的:
-
请先修改config.py里面的DOMAIN为你申请的应用域名;
- 如果你在红墙内,可能需要打开“我需要翻墙(针对中国用户)”开关,默认的翻墙转发器采用作者在heroku上搭建的免费服务器: http://kforwarder.herokuapp.com,如果你有余力或者注重隐私,建议自己搭建一个,源码托管在 https://github.com/cdhigh/forwarder 。(如果将来墙也封锁了heroku,则此翻墙方法无效)
-
-
-
- 有的网站需要登陆才能阅读文章的问题如何解决?
-
- 有些网站需要先注册账户然后登陆后才能阅读和下载文章,对于此类网站,则可以仿照books目录下的预置书籍,增加一个新的py文件,然后设置这几个属性:
-
- needs_subscription
:设置为True
- login_url
:为此网站的登陆网页URL
- form_4_login
:可选(如果你不会看网页HTML源码就暂时不用设置),如果不设置或设置为None则KindleEar尝试自动判断;设置为整数则表示此网页中第几个form为登陆表单(0为第一个);设置为字符串则表示对应登陆表单form的名字,'#'打头表示id,'.'打头表示class,否则则匹配name/id/class任何一个。
- 登陆KindleEar后,在“我的订阅”页面先订阅对应书籍,然后书籍行右边会新出现一个按钮“登陆信息”, 点击后请在新页面中输入正确的账号和密码。
-
- 注1:因为GAE环境的限制,需要执行javascript或要输入验证码的网站则无法支持。
- 注2:对于一些足够特殊和复杂的网站,可能你需要在书籍子类中重写login()函数。
- 注3:你输入的密码将加密保存,密钥为每个账号都不一样的8位随机字符串,有一定的安全性,而且我尽量照顾你的密码安全,你可以随时删除保存的密码信息,书籍退订后也马上删除密码,但因为密钥也保存在GAE上(在另外的数据表),所以不能保证很高的安全性,请自己明白并愿意承担其中的风险。
-
-
-
- 网站在IE6下显示不正常?
- 在IE6下网站排版混乱并且无法显示导航栏,因为网站的排版采用YAHOO的CSS框架pure,而此框架要求IE7以上的IE,或者chrome/firefox,如果你使用的IE版本过低,建议升级到新版本。
-
-
- 内置书籍订阅时“Separate”选项是什么意思?
- 一旦你选择了“合并推送”,则所有订阅源都会合并成一本书籍推送至kindle,如果你需要有部分订阅源分开单独推送,则在订阅时选择“Separate“。
-
-
- 部署应用后打开网页出现“internal server error”如何处理?
-
- 有部分网友在部署后打开此应用的“我的订阅”和“投递日志”网页会出现“internal server error”错误,一般情况下因为gae没有及时生成对应数据库的索引导致。 你可以打开gae后台,查看logs日志,看是否有“NeedIndexError: no matching index found.”异常,如果有的话,证明是没有生成索引。 你也可以在gae后台查看“Datastore Indexes”,看每个条目的“Status”,正常情况下应该都是“Serving”,如果是“Building”或其他值,则说明索引也没有正确生成。
- 如果你部署步骤完全正确的话,可以稍等几分钟到几十分钟,等gae自动生成索引,如果gae没有成功自动生成索引,你也可以手动更新索引:
- python appcfg.py update_indexes KindleEar/
- 在终端窗口执行此命令,注意最后一个KindleEar是你保存的此应用的目录名。
- 如果出现的异常信息中有“UnknownQueueError”,则说明部署有问题,可以先单独执行此命令上传更新队列配置信息:
- python appcfg.py update_queues KindleEar/
- 这几个命令也可以执行一遍试试。(比如手动可以推送无法自动推送则执行 update_cron)
-
- python appcfg.py update_cron KindleEar/
- python appcfg.py update_queues KindleEar/
- python appcfg.py update_dispatch KindleEar/
-
- 如果上面的步骤执行完后还是不行的话,请按照正确步骤重新部署。
-
-
-
- 如何修改多本书籍合并后的封面?
- 默认配置下如果合并多本书籍,则合并后的封面会将所有书籍的封面缩小并拼贴在一起,并且每次推送的排列顺序都不一样,此效果由config.py里面的参数项 DEFAULT_COVER_BV 控制,默认为 None。
- 如果你不喜欢这个效果,设置DEFAULT_COVER_BV为某个文件名则可以取消此效果,比如:
- DEFAULT_COVER_BV='cv_bound.jpg'
-
-
-
- 如何订阅微信公众号?
- 网站 今天看啥 提供微信公众号转RSS服务(注意在添加RSS时选择isfulltext选项)。
-
-
-
- 有关墙的汇总
- 在天朝的网友在部署时和使用时会遇到一个强大的墙,特别google又是特别重要的黑名单,这是几个简单的说明,其他的请自己解决:
-
-
-
-
- 保存到Pocket功能如何使用?
- Pocket 是一个比较流行的稍后阅读应用,你在电子书上读到有趣或有用的文章后,可以选择将其发送到你的Pocket账号,然后就可以通过跨平台的Pocket应用来继续阅读。 要启动此功能,请按如下步骤打开:
-
- 申请Pocket账号;
- 点击网站上的"高级设置" | "归档与分享"页面中的 'Save to Pocket' 区段的[申请授权],然后浏览器会跳转到Pocket的页面进行授权,在你确认授权后会自动跳转回来,则完成申请授权过程;
- 在KindleEar的“高级设置” | "归档与分享"页面中,勾选“在每篇文章后附加 'Save to Pocket' 超链接”。
-
- 备注:
-
- Pocket对应用授权采用 Oauth 2.0 技术,只有Pocket才知道你的账号和密码;
- 此应用仅申请了添加文章权限,没有权限修改和删除你的账户中的已有文章,也没有权限查看你的文章列表;
- 在Pocket申请到的授权码保存在你自己的应用数据库中,其他人搭建的KindleEar没有权限访问你的账户;
- 如果你更注重隐私和安全,你也可以自己在Pocket上 申请一个开发者的Consumer Key ,然后修改config.py中的POCKET_CONSUMER_KEY。
- 如果需要反向将Pocket的文章推送至Kindle,可以打开Pocket的自定义RSS功能,在Pocket控制面板的“隐私控制”|“RSS 订阅源”可以找到你的专有RSS订阅地址,把它加入KindleEar的订阅列表中即可。
- 而因Instapaper仅提供了简单的认证API,所以 'Save to Instapaper ' 功能需要输入账号和密码,但是密码是加密后保存在你自己的应用的数据库中的。
-
-
-
-
- 投递日志状态wrong SRC_EMAIL的解决方案。
- 到Gae后台的Settings页面,看看 已经授权的Email列表里面有没有你的发送邮箱地址,如果没有就添加即可。
-
-
-
- 其他的一些小Tips。
-
-
- 设置界面上的邮箱地址可以设置为多个,用分号隔开即可。同样,分享界面上的邮箱地址也可以设置为多个。
- 如果推送到手机上观看,可以设置config.py里面的COLOR_TO_GRAY=False,以便查看彩色图像。
- 自定义书籍中的属性 extra_css 可以自定义投递文章的样式。
- 在高级设置中可以批量导入和导出OPML格式的订阅列表,这功能比较适合从其他RSS软件中转移数据。
- 因Evernote的免费政策,通过邮件保存到Evernote仅支持5个邮件,需要更多则需要升级Evernote账户。
-
-
-
-
- 漫画订阅说明
-
- 目前支持以下网站的漫画
-
- 漫画的订阅方法有两种,可以选择其中任意一种。
- 这里使用【食戟之灵】举例,先从动漫狂中搜索得到该漫画的URL。
-
- 1. 通过Feeds订阅
- 这种方法比较简单,在【我的订阅】页面的【自定义RSS】框中,添加书籍标题和URL进行订阅。
- 注意:投递时需要打开【自动定时投递自定义RSS】选项。
-
- 2. 新建书籍并上传订阅
- 进入books/comic/下,在该目录拷贝conan.py并任意命名,修改其中的下列位置以后,重新上传到GAE,点击新的书籍【[漫画]食戟之灵】进行订阅。
- 拷贝conan.py到soma.py。
-
-
- 位置
- 修改前
- 修改后
- 说明
-
-
- 7行
- return Conan
- return Soma
- 使用"Soma"作为类名
-
-
- 9行
- class Conan(CartoonMadBaseBook):
- class Soma(CartoonMadBaseBook):
- 使用"Soma"作为类名
-
- 10行
- title = u'[漫画]名侦探柯南'
- title = u'[漫画]食戟之灵'
- 漫画名为【食戟之灵】
-
- 11行
- description = u'日本漫画家青山刚昌创作的侦探漫画'
- description = u'由附田祐斗原作,佐伯俊作画的连载作品。'
- 漫画的简单说明
-
- 17行
- feeds = [(u'[漫画]名侦探柯南', 'http://www.cartoonmad.com/comic/1066.html')]
- feeds = [(u'[漫画]食戟之灵', 'http://www.cartoonmad.com/comic/1698.html')]
- 在网站上搜索到该漫画的网址
-
-
-
-
-
- 使用sendgrid发送邮件
-
- 在设置界面,可以设置sendgrid发送邮件,需要在下面网站申请帐号并创建自己的apikey。
- sendgrid
- 推荐按照Google的官方说明进行操作。
- google
- 需要说明的是sendgrid支持的邮件大小是30M,但是由于gcloud的urlfetch的限制变成了10M。
-
-
- 我还有更多问题,到哪里去问?
- 如果你碰到更多问题,可以到 https://github.com/cdhigh/KindleEar/issues 去提交一个issue,然后等待答复,不过在提交问题之前,还是建议先搜索一下别人先前提交的issues,说不定已经有人重复提过了相关问题呢?
- 如果没有别人提过类似问题,你在提交新问题的时候,建议附上 GAE后台 的Logs信息,以便定位问题,也可以得到更快的答复。
-
-
-
-
-
diff --git a/application/static/faq_en.html b/application/static/faq_en.html
deleted file mode 100644
index 88d863b2..00000000
--- a/application/static/faq_en.html
+++ /dev/null
@@ -1,309 +0,0 @@
-
-
-
-
- FAQ - KindleEar
-
-
-
-
-
-
-
-
-
- What is KindleEar?
- KindleEar is a free webapp that running on Google App Engine, it aggregates feeds and deliver the generated docs to your Kindle periodically.
- Features:
-
- Custom rss
- Support full text rss and summary rss
- Multi-account management, which means that it supports several kindle devices
- The magazine format MOBI with images
- Automatically daily scheduled delivery
- Built-in shared library, can share links with others and subscribe links from others
- Powerful email service
- Integration with Evernote/Pocket/Instapaper etc
-
- Benefits:
-
- All free. Not only this application is free, but GAE provides free hosting space, you do not need to pay a penny.
- Generate fine magazine format books. Sample as follows (the left one is on kindle3, the right two are on kindle touch):
-
-
-
-
-
- Dows this application require its own server?
- This application is hosted on Google App Engine (GAE), GAE has free quotas enough for small applications like KindleEar, you don't need to pay a penny to run KindleEar in GAE, it's fine if you don't own a server(VPS or other infrastructure).
- Remember: GAE(Google App Engine) is not GCE (Google Computer Engine), GAE is free forever (with limit of quotas of resources), GCE is free only within 60 days trial. you don't need to start a GCE trial.
-
-
-
- How to deploy my own delivery server?
-
-
- Download the latest version of KindleEar on github and then extract it to somewhere, for example C:\KindleEar (we'll use it in the following steps).
- You need a google account, please visit the Google Developers Console page. Use your gmail account to login, click on "Create an empty Project", follow steps to create a GAE application.
- Install Python 2.7.x. Download the latest 2.7.x source release and then install it. Do not install a Python 3.x version, because KindleEar does not support it now.
- Install GAE SDK , choose some version before 273. for example:
-
- google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip
- google-cloud-sdk-273.0.0-darwin-x86.tar.gz
- google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz
- google-cloud-sdk-273.0.0-linux-x86.tar.gz
- google-cloud-sdk-273.0.0-linux-x86_64.tar.gz
- google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip
- google-cloud-sdk-273.0.0-windows-x86.zip
- google-cloud-sdk-273.0.0-windows-x86_64.zip
-
-
- Modify variables 'SRC_EMAIL' and 'DOMAIN' and 'TIMEZONE' in config.py in same directory of app.yaml. You can modify other variables if you want and understand what it means.Using the default values are recommended.
- Start upload KindleEar now.
-
- Followed by clicking: Start - Run - type cmd, it will open a command line window
- Change current directory to App Engine SDK installation directory, example: cd C:\Program Files\Google\google_appengine
- gcloud auth login
- gcloud config set project YourAppId
- gcloud app deploy --version=1 app.yaml module-worker.yaml
- gcloud app deploy --version=1 KindleEarDirectory
- If something goes wrong, execute following command lines.
-
- gcloud datastore indexes create index.yaml
- gcloud app deploy --version=1 app.yaml queue.yaml
- gcloud app deploy --version=1 app.yaml cron.yaml
- gcloud app deploy --version=1 app.yaml dispatch.yaml
-
-
-
-
- After successfully uploading KindleEar, use your browser to open the site: http://kindleear123.appspot.com (example only, kindleear123 should be changed to the name of your GAE application). If everything goes fine, then go to the last step.
- Add your gmail account to your kindle email whitelist. You should know how to do it already.
- Then what? Enjoy your own delivery service now!
- Tips: the initial username of application is admin, default password is admin too.
-
- The easy way(recommended):
-
- Download KindleEar and extract it, get a GAE account, create an application.
- Download uploader . (for windows user only)
- Extract 'uploader' to a directory, move the folder of KindleEar into the folder of 'uploader' and rename it to kindleear, then double-click 'uploader.bat' to upload.
- Tips: the initial username of application is admin, default password is admin too.
-
-
-
-
- Why do I not receive the deliveries?
- There are many possible reasons:
-
- You haven't enabled the "Enable deliver custom rss" option, this option is at the bottom of the setting page.
- You haven't added your gmail address to Amazon's 'Approved Personal Document E-mail List' .
- The content of the rss link isn't a valid XML format.
- For those feeds generated by Feedburner, you can append a query string '?format=xml' to end of url, for example: http://xx.com/feed?format=xml.
-
-
-
- What is full text rss?
- Full text rss means that it contains all the contents of the articles in the rss, while summary rss contains only brief descriptions of the articles. You can check it by opening the link of the rss.
-
-
- Can full text rss be processed as summary rss? What about the reverse?
- Full text rss can be treated as summary rss. In this case, program will ignore the content in the rss xml file and fetch full text from the original link. It costs much time which will result the decreasing of supported rss quantities. If summary rss is processed as full text rss, articles sent to your kindle would still have brief descriptions only.
-
-
- How to deal with weekly subscription?
- Some rss are updated weekly. For this type of rss, you can touch a new python file in the 'books' directory with variable 'deliver_days' setted to a day.
- deliver_days=['Friday']
-
-
- Can KindleEar deliver news to my kindle more than once a day?
- The answer is yes, follow the next steps to make it come true:
-
- Add a python file to the directory of 'books'.
- Set 'deliver_times' using a list in the new python file.
- For example: deliver_times=[6,14,22] #6:00,14:00,22:00 deliver three times a day
-
- Optional: set 'oldest_article' to the number of interval seconds between two deliveries.
- For example: oldest_article=28800 #8*60*60
-
-
-
-
-
- How to use the url filter?
- Url filter is not for beginners, you need to know regular expression and python. Urls in the list would not be downloaded for saving time.
-
-
- What can I do if the gfw of China blocked some rss?
- Change the link to use https protocol.
-
-
- How to put the cover or the masthead images in the same directory when I add a customize book?
- Set the variable mastheadfile and coverfile to a path, for example:
- mastheadfile = 'books/xxxx.gif'
- coverfile = 'books/xxxx.jpg'
-
-
-
- How to deal with the SSL error problem when I upload KindleEar?
- Some friends in China would encounter "urllib2.URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in violation of protocol>". To solve this problem, try to add a new line "74.125.229.174 appengine.google.com" to the hosts file.
-
-
- Why some articles have garbled characters?
- For custom rss and the built-in books without encoding specified, program detects encoding automatically by using http header, html meta or the chardet module of python. Howerver, the result is not 100% accurate. If the encoding detecting is wrong, the article may be garbled. Because this situation is very rare, KindleEar only supplies an not very elegant solution: manually change the chardet-detect encoding saved in the database. Steps as follows:
-
Login appid.appspot.com by using account 'admin'(assuming your application name is appid).
- Visit 'https://appid.appspot.com/dbviewer'. Check the encoding of the website whose articles are garbled in the UrlEncoding section. If the encoding is wrong, take down its ID.
- Now visit 'https://appid.appspot.com/dbviewer?action=modurlenc&id=dbid&feedenc=fenc&pageenc=penc'
- Before you press enter, modify some vaiables:
- dbid: the ID you just took down.
- fenc: new correct FeedEncoding.
- penc: new correct PageEncoding.
-
- Or you can change them in 'Datastore viewer' in the GAE backend.
- You can get the correct encoding by reading source code of a webpage.
- If you cannot find the website in page 'dbviewer', it means KindleEar has never detected the encoding of any page of this website, http header or html meta is used only. In this case, you can set the variable 'ALWAYS_CHAR_DETECT' in 'config.py' to True.
- If the article still has garbled characters after setting 'ALWAYS_CHAR_DETECT' to True, it means that encoding detected by chardet is wrong and doesn't match with the http header or the html meta. The final attempt is to set 'TRUST_ENCODING_IN_HEADER_OR_META' to True.
- With all the solutions tested and the problem still exists, please add the rss to a python file in the folder 'books' with encoding specified.
-
-
-
-
- What can I do if I forgot the password?
- If you are an non-administrator user, you can ask the administrator to change your password. If forgot the administrator's password, you can login App Engine , select "Datastore | Entries" on the left menu, select "KeUser" in the "Kind" Combobox, write down the value of field "secret_key" of database line "admin", let's assume that is "abcdefgh", add some characters at the beginning, become "123abcdefgh", find a website to calculate the MD5 hash of string "123abcdefgh", will be "8b920d97ca5fceff6c5835223c541bf4", fill it in field "passwd" of database line "admin". and then use admin/123 to login.
-
-
-
How to use mail service xxx@appid.appspotmail.com?
-
- You'll have uncountable mail addresses like xxx@appid.appspotmail.com as long as xxx is valid ascii string after you have deployed KindleEar(appid is your application id).
-
- Before you can enjoy the feature, please add email addresses to the whitelist in menu 'Advance'|'White List'. A asterisk '*' means allow anyone to trigger it, or only allow some people by adding their emaill addresses like 'john@example.com/susan@example.com'.
- KindleEar will convert the text of the email received from xxx@appid.appspotmail.com as an attachment and then send it to your registered email address. If there are only hyperinks in the email, KindleEar will first fetch the url, generate a book and then send it.
- Add flag !links to end of subject to indicate KindleEar crawl all links in email and transfer to your Kindle.
- Add flga !article to end of subject to indicate KindleEar ignore all links in email.
- The generated book will be sent to admin's mailbox by default. Use username__xxx@appid.appspotmail.com(Note: there're two underscores) if you want the book to be sent to other registered users.
- Send emails with the download link of an ebook to book@appid.appspotmail.com or username__book@appid.appspotmail.com will trigger a task to download the ebook and then diliver it to kindle directly.
- Refer to Mail Python API Overview to get to know the list of allowed postfix of ebooks.
- Send emails to trigger@appid.appspotmail.com or username__trigger@appid.appspotmail.com will trigger a delivery by hand.It equals to the 'deliver now' button in 'Setting' Menu if the subject is empty or is 'all'.Deliver specific books by adding the name of the book, seperate with commma if there is more than one.
-
-
-
-
- Why I can't lookup words in the delivery books after I chosen 'Merge books into one'?
- With the option 'Merge books in one' checked, KindleEar will set the metadata of the book as Custom Rss. To fix this problem, you have to set the language attribute in the 'Custom Rss Setting' section manually.
-
-
-
- How to deal with those websites that need login before I can view the articles?
-
- If you want to crawl a website that needs login, you have to add a python file to the folder 'books'. Refer to sample books if you don't know how to write it.
-
- needs_subscription
: Set to True
- login_url
: The URL for login page
- form_4_login
: Optional, if not set, KindleEar will guess which form in the page is used to login. Set to an integer indicate the sequence number (where 0 is the first) of the form in the page. Set to a string indicate the name of the form, you can use CSS selector: '#' for id and '.' for class, string only will match any of it(name/id/class).
- Then login to KindleEar. In the 'Feeds' page, a new 'Login info' button will appear on the right side of the book which needs_subscription setted to True in the 'Subscripted' section. Click on it and then fill in your account name and password.
-
- Note1: currently we don't support those sites that need run javascript to login or need to input a captcha.
- Note2: for some special site, you may have to overwirte the 'login' function which inherits from the 'BaseFeedBook' class.
- Note3: the password will be stored encrypted on GAE, the key for encryption is a random string for each account of KindleEar, but the key is also stored on GAE, so it's not absolutely safe, please keep this in mind.
-
-
-
- Can I delete/hide books in Chinese?
-
- If you are not Chinese, you can delete some *.py files in directory 'books'. '__init__.py' and 'base.py' are required, please don't remove them.
- If you deploy the application via uploader, uploader can help you to delete books which language is Chinese.
-
-
-
- What's the meaning of the "Separate" option for the built-in books?
- If you choose this option, the book you chosen will be sent to kindle separately even if the option "Merge books into one" is chosen.
-
-
- How to solve it if website shows "internal server error" exception.
-
- Open appengine.google.com , choose your application, open "Logs", if have “NeedIndexError: no matching index found.” exception, then you can execute command in your computer.
- python appcfg.py update_indexes YourKindleEarFolder/
- if have “UnknownQueueError” exception, execute:
- python appcfg.py update_queues YourKindleEarFolder/
-
- After command be executed, please wait some miniutes for GAE generate index for you.
- If the problem still exist, you should deploy KindleEar again.
-
-
-
- How to use the feature 'Save to Pocket'?
- The Pocket is a widely used 'Read It Later' service that enables users to save articles or web pages to the cloud for future reading. These articles are then synced to the user's Pocket list, accessible across all their devices, facilitating offline reading. Pocket streamlines articles by removing clutter and provides customizable text settings for a more comfortable reading experience.Now, you have the option to save a Kindle article to your Pocket account, allowing you to read it on other devices at your convenience.
- Follows steps to enjoy this feature:
-
- Create a Pocket account;
- Click on [Authorize] in the 'Save to Pocket' section on KindleEar's 'Advanced' | 'Archive' page;
- Select "Append hyperlink 'Save to Pocket' to article".
-
-
- Remarks:
-
- Authorization utilizes OAuth v2 technology, ensuring that only Pocket has access to your username and password;
- This application requests only 'Add' permission, without asking for 'Modify' or 'Delete', and it cannot retrieve your article list;
- The Pocket's access token is stored in your database, preventing other KindleEar applications from accessing your Pocket account;
- If you are concerned about privacy and security, you can apply for a new developer Consumer Key on the Pocket website and then update the variable POCKET_CONSUMER_KEY in config.py;
- If you want KindleEar to deliver your unread articles to KindleEar, find an RSS address on the Pocket page and add it to the list of subscriptions in KindleEar;
- Both the username and password are required to activate the 'Save to Instapaper ' feature, but the password is encrypted before being saved to the database.
-
-
-
-
-
- My logs shows error 'wrong SRC_EMAIL'?
-
- Go to your GAE console , in 'Settings' | 'Application settings' page, add your email into field 'Email API authorized senders'.
-
-
-
-
-
diff --git a/application/static/periodical.gif b/application/static/periodical.gif
deleted file mode 100644
index a3c57354..00000000
Binary files a/application/static/periodical.gif and /dev/null differ
diff --git a/application/static/separate.gif b/application/static/separate.gif
deleted file mode 100644
index 6edc7dbe..00000000
Binary files a/application/static/separate.gif and /dev/null differ
diff --git a/application/templates/adv_archive.html b/application/templates/adv_archive.html
index 223371cb..05994172 100644
--- a/application/templates/adv_archive.html
+++ b/application/templates/adv_archive.html
@@ -18,7 +18,7 @@
{% if evernote.get('enable') %} checked="1" {% endif %} />
{{appendStrs['Evernote']}}
-
+
@@ -26,7 +26,7 @@
{{appendStrs["Wiz"]}}
-
+
@@ -78,10 +78,6 @@
{{appendStrs["Weibo"]}}
-
-
- {{appendStrs["TencentWeibo"]}}
-
{{appendStrs["Browser"]}}
diff --git a/application/templates/adv_uploadcover.html b/application/templates/adv_uploadcover.html
index 9422599f..174bc57e 100644
--- a/application/templates/adv_uploadcover.html
+++ b/application/templates/adv_uploadcover.html
@@ -41,6 +41,8 @@
+
+
{% endblock -%}
diff --git a/application/templates/base.html b/application/templates/base.html
index 217ecc99..d6d1a179 100644
--- a/application/templates/base.html
+++ b/application/templates/base.html
@@ -7,9 +7,9 @@
{% block titleTag -%}
{{ _(title) }} - KindleEar
{% endblock -%}
-
-
-
+
+
+
diff --git a/application/templates/logs.html b/application/templates/logs.html
index 459d0757..68a88c84 100644
--- a/application/templates/logs.html
+++ b/application/templates/logs.html
@@ -33,9 +33,13 @@
{{log.size|filesizeformat}}
{{log.to}}
-
- {{ log.status }}
-
+ {% if log.status == 'ok' -%}
+ ok
+ {% elif log.status|length > 15 -%}
+ {{log.status[:12]}}...{{log.status}}
+ {% else -%}
+ {{ log.status }}
+ {% endif -%}
{% endfor -%}
diff --git a/application/templates/setting.html b/application/templates/setting.html
index c7978fa5..f3f8df3d 100644
--- a/application/templates/setting.html
+++ b/application/templates/setting.html
@@ -189,6 +189,9 @@
{% if 'sendgrid' in sm_services -%}
sendgrid
{% endif -%}
+ {% if 'mailjet' in sm_services -%}
+ mailjet
+ {% endif -%}
{% if 'smtp' in sm_services -%}
SMTP
{% endif -%}
@@ -199,6 +202,10 @@
{{_("ApiKey")}}
+
+ {{_("SecretKey")}}
+
+
{{_("Host")}}
@@ -209,11 +216,11 @@
{{_("Username")}}
-
+
{{_("Password")}}
-
+
{{_("Save path")}}
@@ -221,11 +228,17 @@
{% endif -%}
+ {% if user.sender -%}
{% autoescape off -%}
- {{_("Important: Please activate your kindle firstly, then goto %(personal)s Page and add %(sender)s to 'Approved Personal Document E-mail List'.", personal='' + _("Personal Document Settings") + ' ', sender='' + src_mail + ' ')|safe}}
+ {{_("Important: Please activate your kindle firstly, then goto %(personal)s Page and add %(sender)s to 'Approved Personal Document E-mail List'.", personal='' + _("Personal Document Settings") + ' ', sender='' + user.sender + ' ')|safe}}
{% endautoescape -%}
+ {% else -%}
+
+ {{_("You have not yet set up your email address. Please go to the 'Admin' page to add your email address firstly.")}}
+
+ {% endif %}
@@ -248,6 +261,7 @@
var svr = $('#sm_service').val();
if (svr == 'gae') {
$('#sm_apikey').hide();
+ $('#sm_secret_key').hide();
$('#sm_host').hide();
$('#sm_port').hide();
$('#sm_username').hide();
@@ -255,6 +269,15 @@
$('#sm_save_path').hide();
} else if (svr == 'sendgrid') {
$('#sm_apikey').show();
+ $('#sm_secret_key').hide();
+ $('#sm_host').hide();
+ $('#sm_port').hide();
+ $('#sm_username').hide();
+ $('#sm_password').hide();
+ $('#sm_save_path').hide();
+ } else if (svr == 'mailjet') {
+ $('#sm_apikey').show();
+ $('#sm_secret_key').show();
$('#sm_host').hide();
$('#sm_port').hide();
$('#sm_username').hide();
@@ -262,6 +285,7 @@
$('#sm_save_path').hide();
} else if (svr == 'smtp') {
$('#sm_apikey').hide();
+ $('#sm_secret_key').hide();
$('#sm_host').show();
$('#sm_port').show();
$('#sm_username').show();
@@ -269,6 +293,7 @@
$('#sm_save_path').hide();
} else if (svr == 'local') {
$('#sm_apikey').hide();
+ $('#sm_secret_key').hide();
$('#sm_host').hide();
$('#sm_port').hide();
$('#sm_username').hide();
@@ -276,6 +301,7 @@
$('#sm_save_path').show();
} else {
$('#sm_apikey').hide();
+ $('#sm_secret_key').hide();
$('#sm_host').hide();
$('#sm_port').hide();
$('#sm_username').hide();
diff --git a/application/translations/tr_TR/LC_MESSAGES/messages.mo b/application/translations/tr_TR/LC_MESSAGES/messages.mo
index a337ad96..06f2745a 100644
Binary files a/application/translations/tr_TR/LC_MESSAGES/messages.mo and b/application/translations/tr_TR/LC_MESSAGES/messages.mo differ
diff --git a/application/translations/tr_TR/LC_MESSAGES/messages.po b/application/translations/tr_TR/LC_MESSAGES/messages.po
index 0dde0670..289fe0ed 100644
--- a/application/translations/tr_TR/LC_MESSAGES/messages.po
+++ b/application/translations/tr_TR/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2024-02-27 22:09-0300\n"
+"POT-Creation-Date: 2024-03-10 21:31-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language: tr_TR\n"
@@ -76,15 +76,15 @@ msgstr "Hesaplar"
#: application/templates/admin.html:43
#: application/templates/adv_whitelist.html:29 application/templates/my.html:32
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add"
msgstr "Ekle"
#: application/templates/admin.html:54 application/templates/home.html:15
-#: application/templates/login.html:21 application/templates/logs.html:61
+#: application/templates/login.html:21 application/templates/logs.html:65
#: application/templates/reset_password.html:19
#: application/templates/reset_password.html:20
-#: application/templates/setting.html:211 application/templates/signup.html:21
+#: application/templates/setting.html:218 application/templates/signup.html:21
#: application/templates/user_account.html:15
msgid "Username"
msgstr "Kullanıcı adı"
@@ -190,7 +190,7 @@ msgstr "Kullanıcı adı"
#: application/templates/adv_archive.html:55 application/templates/base.html:47
#: application/templates/home.html:16 application/templates/login.html:25
-#: application/templates/setting.html:215 application/templates/signup.html:25
+#: application/templates/setting.html:222 application/templates/signup.html:25
#: application/templates/user_account.html:19
msgid "Password"
msgstr "Şifre"
@@ -199,8 +199,8 @@ msgstr "Şifre"
msgid "Verify"
msgstr "Doğrulamak"
-#: application/templates/adv_archive.html:93
-#: application/templates/setting.html:230
+#: application/templates/adv_archive.html:89
+#: application/templates/setting.html:243
msgid "Save settings"
msgstr "Ayarları kaydet"
@@ -486,8 +486,8 @@ msgstr "Kindle ile oku"
msgid "Verified"
msgstr "Doğrulanmış"
-#: application/templates/base.html:59 application/view/login.py:75
-#: application/view/share.py:145
+#: application/templates/base.html:59 application/view/login.py:72
+#: application/view/share.py:154
msgid "The username does not exist or password is wrong."
msgstr "Kullanıcı adı mevcut değil veya şifre yanlış."
@@ -583,14 +583,14 @@ msgstr "Tarif için özel teslimat zamanı başarıyla kaydedildi."
msgid "The account have been deleted."
msgstr "Hesap silindi."
-#: application/templates/base.html:83 application/view/admin.py:199
-#: application/view/share.py:135
+#: application/templates/base.html:83 application/view/admin.py:201
+#: application/view/share.py:144
msgid "The username or password is empty."
msgstr "Kullanıcı adı veya şifre boş."
#: application/templates/base.html:84 application/view/admin.py:86
-#: application/view/admin.py:163 application/view/admin.py:203
-#: application/view/login.py:211 application/view/login.py:271
+#: application/view/admin.py:165 application/view/admin.py:205
+#: application/view/login.py:213 application/view/login.py:273
msgid "The two new passwords are dismatch."
msgstr "İki yeni şifre uyuşmuyor."
@@ -602,7 +602,7 @@ msgstr "Şifre başarıyla değiştirildi."
msgid "Account added successfully."
msgstr "Hesap başarıyla eklendi."
-#: application/templates/base.html:87 application/view/login.py:121
+#: application/templates/base.html:87 application/view/login.py:123
msgid "login required"
msgstr "Giriş yapılması gerekiyor"
@@ -722,8 +722,8 @@ msgstr ""
msgid "Search"
msgstr "Ara"
-#: application/templates/login.html:34 application/view/login.py:188
-#: application/view/login.py:195
+#: application/templates/login.html:34 application/view/login.py:190
+#: application/view/login.py:197
msgid ""
"The website does not allow registration. You can ask the owner for an "
"account."
@@ -735,11 +735,11 @@ msgstr ""
msgid "Only display last 10 logs"
msgstr "Sadece son 10 işlem kaydını gösterir."
-#: application/templates/logs.html:21 application/templates/logs.html:62
+#: application/templates/logs.html:21 application/templates/logs.html:66
msgid "Time"
msgstr "Tarih/Saat"
-#: application/templates/logs.html:22 application/templates/logs.html:63
+#: application/templates/logs.html:22 application/templates/logs.html:67
#: application/templates/my.html:17 application/templates/setting.html:98
#: application/templates/setting.html:99 application/templates/setting.html:100
#: application/templates/setting.html:101
@@ -747,23 +747,23 @@ msgstr "Tarih/Saat"
msgid "Title"
msgstr "Başlık"
-#: application/templates/logs.html:23 application/templates/logs.html:64
+#: application/templates/logs.html:23 application/templates/logs.html:68
msgid "Size"
msgstr "Dosya Boyutu"
-#: application/templates/logs.html:24 application/templates/logs.html:65
+#: application/templates/logs.html:24 application/templates/logs.html:69
msgid "To"
msgstr "Kime"
-#: application/templates/logs.html:25 application/templates/logs.html:66
+#: application/templates/logs.html:25 application/templates/logs.html:70
msgid "Status"
msgstr "Durum"
-#: application/templates/logs.html:45
+#: application/templates/logs.html:49
msgid "There is no log"
msgstr "Kayıt yok."
-#: application/templates/logs.html:49
+#: application/templates/logs.html:53
msgid "Logs of other users"
msgstr "Diğer kullanıcıların işlem kayıtları"
@@ -938,23 +938,27 @@ msgstr "Posta servisi gönder"
msgid "Service"
msgstr "Servis"
-#: application/templates/setting.html:199
+#: application/templates/setting.html:202
msgid "ApiKey"
msgstr "ApiKey"
-#: application/templates/setting.html:203
+#: application/templates/setting.html:206
+msgid "SecretKey"
+msgstr "SecretKey"
+
+#: application/templates/setting.html:210
msgid "Host"
msgstr "Ana bilgisayar"
-#: application/templates/setting.html:207
+#: application/templates/setting.html:214
msgid "Port"
msgstr "Port"
-#: application/templates/setting.html:219
+#: application/templates/setting.html:226
msgid "Save path"
msgstr "Kayıt yolu"
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
#, python-format
msgid ""
"Important: Please activate your kindle firstly, then goto %(personal)s "
@@ -964,10 +968,18 @@ msgstr ""
"Sayfasına gidin ve %(sender)s'yi 'Onaylanmış Kişisel Belge E-posta "
"Listesi'ne ekleyin."
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
msgid "Personal Document Settings"
msgstr "Ayarları kaydet"
+#: application/templates/setting.html:239
+msgid ""
+"You have not yet set up your email address. Please go to the 'Admin' page"
+" to add your email address firstly."
+msgstr ""
+"Email adresinizi henüz ayarlamadınız. Lütfen e-posta adresinizi eklemek "
+"için 'Yönetim' sayfasına öncelikle gidin."
+
#: application/templates/signup.html:38
msgid "Invitation code"
msgstr "Davetiye kodu"
@@ -981,70 +993,70 @@ msgid "Settings Saved!"
msgstr "Ayarlar Kaydedildi!"
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add account"
msgstr "Hesap ekle"
-#: application/view/admin.py:68 application/view/admin.py:107
-#: application/view/admin.py:135
+#: application/view/admin.py:68 application/view/admin.py:109
+#: application/view/admin.py:137
msgid "You do not have sufficient privileges."
msgstr "Yeterli yetkiniz yok."
-#: application/view/admin.py:82 application/view/admin.py:150
-#: application/view/login.py:207 application/view/login.py:236
+#: application/view/admin.py:82 application/view/admin.py:152
+#: application/view/login.py:209 application/view/login.py:238
#: application/view/setting.py:61 application/view/setting.py:63
-#: application/view/setting.py:65 application/view/share.py:35
+#: application/view/setting.py:65 application/view/share.py:36
msgid "Some parameters are missing or wrong."
msgstr "Bazı parametreler eksik veya yanlış."
-#: application/view/admin.py:84 application/view/login.py:43
-#: application/view/login.py:213
+#: application/view/admin.py:84 application/view/login.py:40
+#: application/view/login.py:215
msgid "The username includes unsafe chars."
msgstr "Kullanıcı adı güvensiz karakterler içeriyor."
-#: application/view/admin.py:88 application/view/login.py:215
+#: application/view/admin.py:88 application/view/login.py:217
msgid "Already exist the username."
msgstr "Kullanıcı adı zaten var."
-#: application/view/admin.py:91 application/view/admin.py:169
-#: application/view/admin.py:196 application/view/login.py:262
+#: application/view/admin.py:93 application/view/admin.py:171
+#: application/view/admin.py:198 application/view/login.py:264
msgid "The password includes non-ascii chars."
msgstr "Şifre ascii olmayan karakterler içeriyor."
-#: application/view/admin.py:111 application/view/admin.py:132
-#: application/view/admin.py:161
+#: application/view/admin.py:113 application/view/admin.py:134
+#: application/view/admin.py:163
msgid "The username '{}' does not exist."
msgstr "'{}' kullanıcı adı mevcut değil."
-#: application/view/admin.py:127
+#: application/view/admin.py:129
msgid "The password will not be changed if the fields are empties."
msgstr "Alanlar boş bırakılırsa şifre değiştirilmeyecek."
-#: application/view/admin.py:128 application/view/admin.py:186
+#: application/view/admin.py:130 application/view/admin.py:188
msgid "Change account"
msgstr "Hesabı değiştir"
-#: application/view/admin.py:129 application/view/admin.py:187
+#: application/view/admin.py:131 application/view/admin.py:189
msgid "Change"
msgstr "Değiştir"
-#: application/view/admin.py:184
+#: application/view/admin.py:186
msgid "Change success."
msgstr "Değişim başarılı."
-#: application/view/admin.py:201
+#: application/view/admin.py:203
msgid "The old password is wrong."
msgstr "Eski şifre yanlış."
-#: application/view/admin.py:208
-msgid "Change password success."
-msgstr "Şifre değiştirme başarılı."
+#: application/view/admin.py:218
+msgid "Changes saved successfully."
+msgstr "Değişiklikler başarıyla kaydedildi."
#: application/view/adv.py:76 application/view/adv.py:77
#: application/view/adv.py:78 application/view/adv.py:79
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84 application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Append hyperlink '{}' to article"
msgstr "'{}' linkini makaleye ekle"
@@ -1071,7 +1083,6 @@ msgstr "instapaper"
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84
msgid "Share on {}"
msgstr "{} üzerinde paylaş"
@@ -1080,30 +1091,26 @@ msgid "weibo"
msgstr "weibo"
#: application/view/adv.py:81
-msgid "tencent weibo"
-msgstr "tencent weibo"
-
-#: application/view/adv.py:82
msgid "facebook"
msgstr "facebook"
-#: application/view/adv.py:84
+#: application/view/adv.py:83
msgid "tumblr"
msgstr "tumblr"
-#: application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Open in browser"
msgstr "Tarayıcıda aç"
-#: application/view/adv.py:359
+#: application/view/adv.py:358
msgid "Authorization Error! {}"
msgstr "Yetkilendirme Hatası! {}"
-#: application/view/adv.py:382
+#: application/view/adv.py:381
msgid "Success authorized by Pocket!"
msgstr "Pocket tarafından yetkilendirilen başarı!"
-#: application/view/adv.py:388
+#: application/view/adv.py:387
msgid ""
"Failed to request authorization of Pocket! See details "
"below: {}"
@@ -1111,18 +1118,18 @@ msgstr ""
"Pocket yetkilendirme isteği başarısız oldu! Aşağıdaki ayrıntılara "
"bakın: {}"
-#: application/view/adv.py:398
+#: application/view/adv.py:397
msgid "Request type [{}] unsupported"
msgstr "İstek türü [{}] desteklenmiyor"
-#: application/view/adv.py:413
+#: application/view/adv.py:412
msgid "The Instapaper service encountered an error. Please try again later."
msgstr ""
"Instapaper servisi bir hata ile karşılaştı. Lütfen daha sonra tekrar "
"deneyin."
-#: application/view/deliver.py:67 application/view/login.py:158
-#: application/view/share.py:39
+#: application/view/deliver.py:67 application/view/login.py:160
+#: application/view/share.py:40
msgid "The username does not exist or the email is empty."
msgstr "Kullanıcı adı mevcut değil veya e-posta boş."
@@ -1138,9 +1145,9 @@ msgstr "Teslim edilecek tarif yok."
msgid "Cannot fetch data from {}, status: {}"
msgstr "{}, durumundan veri alınamıyor: {}"
-#: application/view/library.py:48 application/view/subscribe.py:191
-#: application/view/subscribe.py:302 application/view/subscribe.py:331
-#: application/view/subscribe.py:339
+#: application/view/library.py:48 application/view/subscribe.py:192
+#: application/view/subscribe.py:303 application/view/subscribe.py:332
+#: application/view/subscribe.py:340
msgid "The recipe does not exist."
msgstr "Tarif mevcut değil."
@@ -1148,73 +1155,73 @@ msgstr "Tarif mevcut değil."
msgid "Please use {}/{} to login at first time."
msgstr "İlk giriş için kullanıcı adı:'{}' ve şifre: '{}'"
-#: application/view/login.py:39
+#: application/view/login.py:36
msgid "Username is empty."
msgstr "Kullanıcı adı boş."
-#: application/view/login.py:41
+#: application/view/login.py:38
msgid "The len of username reached the limit of 25 chars."
msgstr "Kullanıcı adının uzunluğu 25 karakter sınırına ulaştı."
-#: application/view/login.py:76
+#: application/view/login.py:73
msgid "Forgot password?"
msgstr "Forgot password?"
-#: application/view/login.py:137 application/view/login.py:273
+#: application/view/login.py:139 application/view/login.py:275
msgid "The token is wrong or expired."
msgstr "Belirteç yanlış veya süresi dolmuş."
-#: application/view/login.py:140
+#: application/view/login.py:142
msgid "Please input the correct username and email to reset password."
msgstr ""
"Şifreyi sıfırlamak için lütfen doğru kullanıcı adı ve e-posta adresini "
"girin."
-#: application/view/login.py:142
+#: application/view/login.py:144
msgid "The email of account '{name}' is {email}."
msgstr "'{name}' hesabının e-postası {email}."
-#: application/view/login.py:163
+#: application/view/login.py:165
msgid "Reset password success, Please close this page and login again."
msgstr ""
"Şifre sıfırlama başarılı, Lütfen bu sayfayı kapatın ve yeniden giriş "
"yapın."
-#: application/view/login.py:166
+#: application/view/login.py:168
msgid "The email you input is not associated with this account."
msgstr "Girdiğiniz e-posta bu hesapla ilişkilendirilmemiştir."
-#: application/view/login.py:177
+#: application/view/login.py:179
msgid "The link to reset your password has been sent to your email."
msgstr "Şifrenizi sıfırlamak için gerekli bağlantı e-postanıza gönderilmiştir."
-#: application/view/login.py:178
+#: application/view/login.py:180
msgid "Please check your email inbox within 24 hours."
msgstr "Lütfen e-posta gelen kutunuzu 24 saat içinde kontrol edin."
-#: application/view/login.py:209
+#: application/view/login.py:211
msgid "The invitation code is invalid."
msgstr "Davetiye kodu geçersiz."
-#: application/view/login.py:217
+#: application/view/login.py:219
msgid ""
"Failed to create an account. Please contact the administrator for "
"assistance."
msgstr "Bir hesap oluşturulamadı. Yardım için lütfen yöneticiyle iletişime geçin."
-#: application/view/login.py:227
+#: application/view/login.py:229
msgid "Successfully created account."
msgstr "Hesap başarıyla oluşturuldu."
-#: application/view/login.py:238
+#: application/view/login.py:240
msgid "Reset KindleEar password"
msgstr "KindleEar şifrenizi sıfırlama"
-#: application/view/login.py:239
+#: application/view/login.py:241
msgid "This is an automated email. Please do not reply to it."
msgstr "Bu otomatik bir e-postadır. Lütfen yanıt vermeyin."
-#: application/view/login.py:240
+#: application/view/login.py:242
msgid "You can click the following link to reset your KindleEar password."
msgstr ""
"KindleEar şifrenizi sıfırlamak için aşağıdaki bağlantıya "
@@ -1228,142 +1235,142 @@ msgstr "Kindle E-mail adresi gerekli!"
msgid "Title is requied!"
msgstr "Başlık zorunlu!"
-#: application/view/setting.py:124
+#: application/view/setting.py:123
msgid "Chinese"
msgstr "Çince"
-#: application/view/setting.py:125
+#: application/view/setting.py:124
msgid "English"
msgstr "İngilizce"
-#: application/view/setting.py:126
+#: application/view/setting.py:125
msgid "French"
msgstr "Fransızca"
-#: application/view/setting.py:127
+#: application/view/setting.py:126
msgid "Spanish"
msgstr "İspanyolca"
-#: application/view/setting.py:128
+#: application/view/setting.py:127
msgid "Portuguese"
msgstr "Portekizce"
-#: application/view/setting.py:129
+#: application/view/setting.py:128
msgid "German"
msgstr "Almanca"
-#: application/view/setting.py:130
+#: application/view/setting.py:129
msgid "Italian"
msgstr "İtalyanca"
-#: application/view/setting.py:131
+#: application/view/setting.py:130
msgid "Japanese"
msgstr "Japonca"
-#: application/view/setting.py:132
+#: application/view/setting.py:131
msgid "Russian"
msgstr "Rusça"
-#: application/view/setting.py:133
+#: application/view/setting.py:132
msgid "Turkish"
msgstr "Türkçe"
-#: application/view/setting.py:134
+#: application/view/setting.py:133
msgid "Korean"
msgstr "Koreli"
-#: application/view/setting.py:135
+#: application/view/setting.py:134
msgid "Arabic"
msgstr "Arapça"
-#: application/view/setting.py:136
+#: application/view/setting.py:135
msgid "Czech"
msgstr "Çek"
-#: application/view/setting.py:137
+#: application/view/setting.py:136
msgid "Dutch"
msgstr "Flemenkçe"
-#: application/view/setting.py:138
+#: application/view/setting.py:137
msgid "Greek"
msgstr "Yunan"
-#: application/view/setting.py:139
+#: application/view/setting.py:138
msgid "Hindi"
msgstr "Hintçe"
-#: application/view/setting.py:140
+#: application/view/setting.py:139
msgid "Malaysian"
msgstr "Malezyalı"
-#: application/view/setting.py:141
+#: application/view/setting.py:140
msgid "Bengali"
msgstr "Bengal"
-#: application/view/setting.py:142
+#: application/view/setting.py:141
msgid "Persian"
msgstr "Farsça"
-#: application/view/setting.py:143
+#: application/view/setting.py:142
msgid "Urdu"
msgstr "Urduca"
-#: application/view/setting.py:144
+#: application/view/setting.py:143
msgid "Swahili"
msgstr "Svahili"
-#: application/view/setting.py:145
+#: application/view/setting.py:144
msgid "Vietnamese"
msgstr "Vietnam"
-#: application/view/setting.py:146
+#: application/view/setting.py:145
msgid "Punjabi"
msgstr "Pencap"
-#: application/view/setting.py:147
+#: application/view/setting.py:146
msgid "Javanese"
msgstr "Cava"
-#: application/view/setting.py:148
+#: application/view/setting.py:147
msgid "Tagalog"
msgstr "Tagalog"
-#: application/view/setting.py:149
+#: application/view/setting.py:148
msgid "Hausa"
msgstr "Hausa"
-#: application/view/share.py:50 application/view/subscribe.py:239
+#: application/view/share.py:51 application/view/subscribe.py:240
msgid "Unknown command: {}"
msgstr "Bilinmeyen komut: {}"
-#: application/view/share.py:56
+#: application/view/share.py:57
msgid "There is no {} email yet."
msgstr "Henüz {} e-postası bulunmamaktadır."
-#: application/view/share.py:96 application/view/share.py:121
-#: application/view/share.py:143
+#: application/view/share.py:105 application/view/share.py:130
+#: application/view/share.py:152
msgid "Saved to your {} account."
msgstr "{} hesabınıza kaydedildi."
-#: application/view/share.py:99 application/view/share.py:117
-#: application/view/share.py:146
+#: application/view/share.py:108 application/view/share.py:126
+#: application/view/share.py:155
msgid "Failed save to {}."
msgstr "{}'e kaydetme işlemi başarısız oldu."
-#: application/view/share.py:100 application/view/share.py:118
-#: application/view/share.py:147
+#: application/view/share.py:109 application/view/share.py:127
+#: application/view/share.py:156
msgid "Reason :"
msgstr "Neden :"
-#: application/view/share.py:109
+#: application/view/share.py:118
msgid "Unauthorized {} account!"
msgstr "{} tarafından yetkilendirilen başarı!"
-#: application/view/share.py:122
+#: application/view/share.py:131
msgid "See details below:"
msgstr "Aşağıdaki ayrıntılara bakın:"
-#: application/view/share.py:145
+#: application/view/share.py:154
msgid "Unknown: {}"
msgstr "Bilinmeyen: {}"
@@ -1387,27 +1394,27 @@ msgstr "Başlık veya URL boş."
msgid "Failed to fetch the recipe."
msgstr "Tarif alınamadı."
-#: application/view/subscribe.py:123 application/view/subscribe.py:264
+#: application/view/subscribe.py:123 application/view/subscribe.py:265
msgid "Failed to save the recipe. Error:"
msgstr "Tarif kaydedilemedi. Hata:"
-#: application/view/subscribe.py:220
+#: application/view/subscribe.py:221
msgid "You can only delete the uploaded recipe."
msgstr "Yalnızca yüklenen tarifi silebilirsiniz."
-#: application/view/subscribe.py:224
+#: application/view/subscribe.py:225
msgid "The recipe have been subscribed, please unsubscribe it before delete."
msgstr "Tarif abone olunmuş, silmeden önce aboneliği iptal edin."
-#: application/view/subscribe.py:237
+#: application/view/subscribe.py:238
msgid "This recipe has not been subscribed to yet."
msgstr "Bu tarife henüz abone olunmadı."
-#: application/view/subscribe.py:251
+#: application/view/subscribe.py:252
msgid "Can not read uploaded file, Error:"
msgstr "Yüklenen dosya okunamıyor, Hata:"
-#: application/view/subscribe.py:259
+#: application/view/subscribe.py:260
msgid ""
"Failed to decode the recipe. Please ensure that your recipe is saved in "
"utf-8 encoding."
@@ -1415,15 +1422,15 @@ msgstr ""
"Tarif çözümlenemedi. Lütfen tarifinizin utf-8 kodlamasında "
"kaydedildiğinden emin olun."
-#: application/view/subscribe.py:279
+#: application/view/subscribe.py:280
msgid "The recipe is already in the library."
msgstr "Tarif zaten kütüphanede."
-#: application/view/subscribe.py:309
+#: application/view/subscribe.py:310
msgid "The login information for this recipe has been cleared."
msgstr "Bu tarifin giriş bilgileri temizlendi."
-#: application/view/subscribe.py:313
+#: application/view/subscribe.py:314
msgid "The login information for this recipe has been saved."
msgstr "Bu tarifin giriş bilgileri kaydedildi."
diff --git a/application/translations/zh/LC_MESSAGES/messages.mo b/application/translations/zh/LC_MESSAGES/messages.mo
index 82980cc6..a602cdbc 100644
Binary files a/application/translations/zh/LC_MESSAGES/messages.mo and b/application/translations/zh/LC_MESSAGES/messages.mo differ
diff --git a/application/translations/zh/LC_MESSAGES/messages.po b/application/translations/zh/LC_MESSAGES/messages.po
index 4c172fee..eca814d8 100644
--- a/application/translations/zh/LC_MESSAGES/messages.po
+++ b/application/translations/zh/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: KindleEar v3.0.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2024-02-27 22:09-0300\n"
+"POT-Creation-Date: 2024-03-10 21:31-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language: zh\n"
@@ -76,15 +76,15 @@ msgstr "用户账号列表"
#: application/templates/admin.html:43
#: application/templates/adv_whitelist.html:29 application/templates/my.html:32
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add"
msgstr "添加"
#: application/templates/admin.html:54 application/templates/home.html:15
-#: application/templates/login.html:21 application/templates/logs.html:61
+#: application/templates/login.html:21 application/templates/logs.html:65
#: application/templates/reset_password.html:19
#: application/templates/reset_password.html:20
-#: application/templates/setting.html:211 application/templates/signup.html:21
+#: application/templates/setting.html:218 application/templates/signup.html:21
#: application/templates/user_account.html:15
msgid "Username"
msgstr "用户名"
@@ -190,7 +190,7 @@ msgstr "邮箱或用户名"
#: application/templates/adv_archive.html:55 application/templates/base.html:47
#: application/templates/home.html:16 application/templates/login.html:25
-#: application/templates/setting.html:215 application/templates/signup.html:25
+#: application/templates/setting.html:222 application/templates/signup.html:25
#: application/templates/user_account.html:19
msgid "Password"
msgstr "密码"
@@ -199,8 +199,8 @@ msgstr "密码"
msgid "Verify"
msgstr "校验"
-#: application/templates/adv_archive.html:93
-#: application/templates/setting.html:230
+#: application/templates/adv_archive.html:89
+#: application/templates/setting.html:243
msgid "Save settings"
msgstr "保存设置"
@@ -478,8 +478,8 @@ msgstr "在Kindle阅读"
msgid "Verified"
msgstr "已校验"
-#: application/templates/base.html:59 application/view/login.py:75
-#: application/view/share.py:145
+#: application/templates/base.html:59 application/view/login.py:72
+#: application/view/share.py:154
msgid "The username does not exist or password is wrong."
msgstr "用户名不存在或密码错误。"
@@ -575,14 +575,14 @@ msgstr "这个Recipe的自定义推送时间已经设定成功。"
msgid "The account have been deleted."
msgstr "这个账号已经被删除。"
-#: application/templates/base.html:83 application/view/admin.py:199
-#: application/view/share.py:135
+#: application/templates/base.html:83 application/view/admin.py:201
+#: application/view/share.py:144
msgid "The username or password is empty."
msgstr "用户名或密码为空。"
#: application/templates/base.html:84 application/view/admin.py:86
-#: application/view/admin.py:163 application/view/admin.py:203
-#: application/view/login.py:211 application/view/login.py:271
+#: application/view/admin.py:165 application/view/admin.py:205
+#: application/view/login.py:213 application/view/login.py:273
msgid "The two new passwords are dismatch."
msgstr "两个密码不匹配。"
@@ -594,7 +594,7 @@ msgstr "修改密码成功。"
msgid "Account added successfully."
msgstr "添加账号成功。"
-#: application/templates/base.html:87 application/view/login.py:121
+#: application/templates/base.html:87 application/view/login.py:123
msgid "login required"
msgstr "需要登录"
@@ -706,8 +706,8 @@ msgstr "使用开源 %(kindleear)s 应用,您可以部署您自己的网站,
msgid "Search"
msgstr "搜索"
-#: application/templates/login.html:34 application/view/login.py:188
-#: application/view/login.py:195
+#: application/templates/login.html:34 application/view/login.py:190
+#: application/view/login.py:197
msgid ""
"The website does not allow registration. You can ask the owner for an "
"account."
@@ -717,11 +717,11 @@ msgstr "这个网站不支持注册,您可以请求管理员创建账号。"
msgid "Only display last 10 logs"
msgstr "只显示最后10条日志"
-#: application/templates/logs.html:21 application/templates/logs.html:62
+#: application/templates/logs.html:21 application/templates/logs.html:66
msgid "Time"
msgstr "时间"
-#: application/templates/logs.html:22 application/templates/logs.html:63
+#: application/templates/logs.html:22 application/templates/logs.html:67
#: application/templates/my.html:17 application/templates/setting.html:98
#: application/templates/setting.html:99 application/templates/setting.html:100
#: application/templates/setting.html:101
@@ -729,23 +729,23 @@ msgstr "时间"
msgid "Title"
msgstr "书籍标题"
-#: application/templates/logs.html:23 application/templates/logs.html:64
+#: application/templates/logs.html:23 application/templates/logs.html:68
msgid "Size"
msgstr "附件大小"
-#: application/templates/logs.html:24 application/templates/logs.html:65
+#: application/templates/logs.html:24 application/templates/logs.html:69
msgid "To"
msgstr "收件人"
-#: application/templates/logs.html:25 application/templates/logs.html:66
+#: application/templates/logs.html:25 application/templates/logs.html:70
msgid "Status"
msgstr "状态"
-#: application/templates/logs.html:45
+#: application/templates/logs.html:49
msgid "There is no log"
msgstr "还没有投递过"
-#: application/templates/logs.html:49
+#: application/templates/logs.html:53
msgid "Logs of other users"
msgstr "其他用户的日志"
@@ -918,23 +918,27 @@ msgstr "发送邮件服务"
msgid "Service"
msgstr "服务"
-#: application/templates/setting.html:199
+#: application/templates/setting.html:202
msgid "ApiKey"
msgstr "ApiKey"
-#: application/templates/setting.html:203
+#: application/templates/setting.html:206
+msgid "SecretKey"
+msgstr "SecretKey"
+
+#: application/templates/setting.html:210
msgid "Host"
msgstr "主机"
-#: application/templates/setting.html:207
+#: application/templates/setting.html:214
msgid "Port"
msgstr "端口"
-#: application/templates/setting.html:219
+#: application/templates/setting.html:226
msgid "Save path"
msgstr "保存路径"
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
#, python-format
msgid ""
"Important: Please activate your kindle firstly, then goto %(personal)s "
@@ -943,10 +947,16 @@ msgstr ""
"注意:必须首先注册您的Kindle设备,同时请到亚马逊账户中心 %(personal)s 页面,将 %(sender)s 添加到 "
"'已认可的发件人电子邮箱列表'。"
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
msgid "Personal Document Settings"
msgstr "个人文档设置"
+#: application/templates/setting.html:239
+msgid ""
+"You have not yet set up your email address. Please go to the 'Admin' page"
+" to add your email address firstly."
+msgstr "您尚未设置您的email地址,请先到“账号管理”页面添加您的email地址。"
+
#: application/templates/signup.html:38
msgid "Invitation code"
msgstr "邀请码"
@@ -960,70 +970,70 @@ msgid "Settings Saved!"
msgstr "恭喜,保存成功!"
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add account"
msgstr "添加账号"
-#: application/view/admin.py:68 application/view/admin.py:107
-#: application/view/admin.py:135
+#: application/view/admin.py:68 application/view/admin.py:109
+#: application/view/admin.py:137
msgid "You do not have sufficient privileges."
msgstr "您没有足够的权限。"
-#: application/view/admin.py:82 application/view/admin.py:150
-#: application/view/login.py:207 application/view/login.py:236
+#: application/view/admin.py:82 application/view/admin.py:152
+#: application/view/login.py:209 application/view/login.py:238
#: application/view/setting.py:61 application/view/setting.py:63
-#: application/view/setting.py:65 application/view/share.py:35
+#: application/view/setting.py:65 application/view/share.py:36
msgid "Some parameters are missing or wrong."
msgstr "一些参数为空或错误。"
-#: application/view/admin.py:84 application/view/login.py:43
-#: application/view/login.py:213
+#: application/view/admin.py:84 application/view/login.py:40
+#: application/view/login.py:215
msgid "The username includes unsafe chars."
msgstr "用户名包含不安全字符。"
-#: application/view/admin.py:88 application/view/login.py:215
+#: application/view/admin.py:88 application/view/login.py:217
msgid "Already exist the username."
msgstr "此账号已经存在。"
-#: application/view/admin.py:91 application/view/admin.py:169
-#: application/view/admin.py:196 application/view/login.py:262
+#: application/view/admin.py:93 application/view/admin.py:171
+#: application/view/admin.py:198 application/view/login.py:264
msgid "The password includes non-ascii chars."
msgstr "密码包含非ASCII字符。"
-#: application/view/admin.py:111 application/view/admin.py:132
-#: application/view/admin.py:161
+#: application/view/admin.py:113 application/view/admin.py:134
+#: application/view/admin.py:163
msgid "The username '{}' does not exist."
msgstr "账号名 '{}' 不存在。"
-#: application/view/admin.py:127
+#: application/view/admin.py:129
msgid "The password will not be changed if the fields are empties."
msgstr "如果不填写密码,则密码不会被修改。"
-#: application/view/admin.py:128 application/view/admin.py:186
+#: application/view/admin.py:130 application/view/admin.py:188
msgid "Change account"
msgstr "修改用户账号"
-#: application/view/admin.py:129 application/view/admin.py:187
+#: application/view/admin.py:131 application/view/admin.py:189
msgid "Change"
msgstr "修改"
-#: application/view/admin.py:184
+#: application/view/admin.py:186
msgid "Change success."
msgstr "修改成功。"
-#: application/view/admin.py:201
+#: application/view/admin.py:203
msgid "The old password is wrong."
msgstr "原密码错误。"
-#: application/view/admin.py:208
-msgid "Change password success."
-msgstr "修改密码成功。"
+#: application/view/admin.py:218
+msgid "Changes saved successfully."
+msgstr "账号设置已更新。"
#: application/view/adv.py:76 application/view/adv.py:77
#: application/view/adv.py:78 application/view/adv.py:79
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84 application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Append hyperlink '{}' to article"
msgstr "在每篇文章后附加 '{}' 超链接"
@@ -1050,7 +1060,6 @@ msgstr "instapaper"
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84
msgid "Share on {}"
msgstr "分享到 {}"
@@ -1059,45 +1068,41 @@ msgid "weibo"
msgstr "微博"
#: application/view/adv.py:81
-msgid "tencent weibo"
-msgstr "腾讯微博"
-
-#: application/view/adv.py:82
msgid "facebook"
msgstr "facebook"
-#: application/view/adv.py:84
+#: application/view/adv.py:83
msgid "tumblr"
msgstr "tumblr"
-#: application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Open in browser"
msgstr "在浏览器打开"
-#: application/view/adv.py:359
+#: application/view/adv.py:358
msgid "Authorization Error! {}"
msgstr "申请授权过程失败! {}"
-#: application/view/adv.py:382
+#: application/view/adv.py:381
msgid "Success authorized by Pocket!"
msgstr "已经成功获得Pocket的授权!"
-#: application/view/adv.py:388
+#: application/view/adv.py:387
msgid ""
"Failed to request authorization of Pocket! See details "
"below: {}"
msgstr "申请Pocket授权失败! 错误信息参考如下: {}"
-#: application/view/adv.py:398
+#: application/view/adv.py:397
msgid "Request type [{}] unsupported"
msgstr "不支持你请求的命令类型 [{}]"
-#: application/view/adv.py:413
+#: application/view/adv.py:412
msgid "The Instapaper service encountered an error. Please try again later."
msgstr "Instapaper服务器异常,请稍候再试。"
-#: application/view/deliver.py:67 application/view/login.py:158
-#: application/view/share.py:39
+#: application/view/deliver.py:67 application/view/login.py:160
+#: application/view/share.py:40
msgid "The username does not exist or the email is empty."
msgstr "用户名不存在或email为空。"
@@ -1113,9 +1118,9 @@ msgstr "没有需要推送的Recipe。"
msgid "Cannot fetch data from {}, status: {}"
msgstr "无法从 {} 获取数据,状态: {}"
-#: application/view/library.py:48 application/view/subscribe.py:191
-#: application/view/subscribe.py:302 application/view/subscribe.py:331
-#: application/view/subscribe.py:339
+#: application/view/library.py:48 application/view/subscribe.py:192
+#: application/view/subscribe.py:303 application/view/subscribe.py:332
+#: application/view/subscribe.py:340
msgid "The recipe does not exist."
msgstr "此Recipe不存在。"
@@ -1123,69 +1128,69 @@ msgstr "此Recipe不存在。"
msgid "Please use {}/{} to login at first time."
msgstr "初次登录请使用用户名'{}'/密码'{}'。"
-#: application/view/login.py:39
+#: application/view/login.py:36
msgid "Username is empty."
msgstr "账号名为空。"
-#: application/view/login.py:41
+#: application/view/login.py:38
msgid "The len of username reached the limit of 25 chars."
msgstr "用户名超过25字符。"
-#: application/view/login.py:76
+#: application/view/login.py:73
msgid "Forgot password?"
msgstr "忘记密码?"
-#: application/view/login.py:137 application/view/login.py:273
+#: application/view/login.py:139 application/view/login.py:275
msgid "The token is wrong or expired."
msgstr "Token码错误或已经逾期。"
-#: application/view/login.py:140
+#: application/view/login.py:142
msgid "Please input the correct username and email to reset password."
msgstr "请输入正确的用户名和Email来重置密码。"
-#: application/view/login.py:142
+#: application/view/login.py:144
msgid "The email of account '{name}' is {email}."
msgstr "账号 '{name}' 的Email为 {email}"
-#: application/view/login.py:163
+#: application/view/login.py:165
msgid "Reset password success, Please close this page and login again."
msgstr "修改密码成功,请关闭此页面重新登录。"
-#: application/view/login.py:166
+#: application/view/login.py:168
msgid "The email you input is not associated with this account."
msgstr "您输入的email不正确。"
-#: application/view/login.py:177
+#: application/view/login.py:179
msgid "The link to reset your password has been sent to your email."
msgstr "重置密码的邮件已经被发送至你的email。"
-#: application/view/login.py:178
+#: application/view/login.py:180
msgid "Please check your email inbox within 24 hours."
msgstr "请在24小时内检查你的email收件箱。"
-#: application/view/login.py:209
+#: application/view/login.py:211
msgid "The invitation code is invalid."
msgstr "邀请码无效。"
-#: application/view/login.py:217
+#: application/view/login.py:219
msgid ""
"Failed to create an account. Please contact the administrator for "
"assistance."
msgstr "创建账号失败,请联系管理员请求协助。"
-#: application/view/login.py:227
+#: application/view/login.py:229
msgid "Successfully created account."
msgstr "成功创建账号。"
-#: application/view/login.py:238
+#: application/view/login.py:240
msgid "Reset KindleEar password"
msgstr "重置KindleEar密码"
-#: application/view/login.py:239
+#: application/view/login.py:241
msgid "This is an automated email. Please do not reply to it."
msgstr "这个是自动发送的邮件,请勿直接回复。"
-#: application/view/login.py:240
+#: application/view/login.py:242
msgid "You can click the following link to reset your KindleEar password."
msgstr "你可以点击下面的链接来重置你的KindleEar密码。"
@@ -1197,142 +1202,142 @@ msgstr "Kindle E-mail必须填写!"
msgid "Title is requied!"
msgstr "书籍标题不能为空!"
-#: application/view/setting.py:124
+#: application/view/setting.py:123
msgid "Chinese"
msgstr "中文"
-#: application/view/setting.py:125
+#: application/view/setting.py:124
msgid "English"
msgstr "英语"
-#: application/view/setting.py:126
+#: application/view/setting.py:125
msgid "French"
msgstr "法语"
-#: application/view/setting.py:127
+#: application/view/setting.py:126
msgid "Spanish"
msgstr "西班牙语"
-#: application/view/setting.py:128
+#: application/view/setting.py:127
msgid "Portuguese"
msgstr "葡萄牙语"
-#: application/view/setting.py:129
+#: application/view/setting.py:128
msgid "German"
msgstr "德语"
-#: application/view/setting.py:130
+#: application/view/setting.py:129
msgid "Italian"
msgstr "意大利语"
-#: application/view/setting.py:131
+#: application/view/setting.py:130
msgid "Japanese"
msgstr "日语"
-#: application/view/setting.py:132
+#: application/view/setting.py:131
msgid "Russian"
msgstr "俄语"
-#: application/view/setting.py:133
+#: application/view/setting.py:132
msgid "Turkish"
msgstr "土耳其语"
-#: application/view/setting.py:134
+#: application/view/setting.py:133
msgid "Korean"
msgstr "韩语"
-#: application/view/setting.py:135
+#: application/view/setting.py:134
msgid "Arabic"
msgstr "阿拉伯语"
-#: application/view/setting.py:136
+#: application/view/setting.py:135
msgid "Czech"
msgstr "捷克语"
-#: application/view/setting.py:137
+#: application/view/setting.py:136
msgid "Dutch"
msgstr "荷兰语"
-#: application/view/setting.py:138
+#: application/view/setting.py:137
msgid "Greek"
msgstr "希腊语"
-#: application/view/setting.py:139
+#: application/view/setting.py:138
msgid "Hindi"
msgstr "印地语"
-#: application/view/setting.py:140
+#: application/view/setting.py:139
msgid "Malaysian"
msgstr "马来西亚语"
-#: application/view/setting.py:141
+#: application/view/setting.py:140
msgid "Bengali"
msgstr "孟加拉语"
-#: application/view/setting.py:142
+#: application/view/setting.py:141
msgid "Persian"
msgstr "波斯语"
-#: application/view/setting.py:143
+#: application/view/setting.py:142
msgid "Urdu"
msgstr "乌尔都语"
-#: application/view/setting.py:144
+#: application/view/setting.py:143
msgid "Swahili"
msgstr "斯瓦希里语"
-#: application/view/setting.py:145
+#: application/view/setting.py:144
msgid "Vietnamese"
msgstr "越南语"
-#: application/view/setting.py:146
+#: application/view/setting.py:145
msgid "Punjabi"
msgstr "旁遮普语"
-#: application/view/setting.py:147
+#: application/view/setting.py:146
msgid "Javanese"
msgstr "爪哇语"
-#: application/view/setting.py:148
+#: application/view/setting.py:147
msgid "Tagalog"
msgstr "他加禄语"
-#: application/view/setting.py:149
+#: application/view/setting.py:148
msgid "Hausa"
msgstr "豪萨语"
-#: application/view/share.py:50 application/view/subscribe.py:239
+#: application/view/share.py:51 application/view/subscribe.py:240
msgid "Unknown command: {}"
msgstr "未知命令:{}"
-#: application/view/share.py:56
+#: application/view/share.py:57
msgid "There is no {} email yet."
msgstr "还没有设置 {} email."
-#: application/view/share.py:96 application/view/share.py:121
-#: application/view/share.py:143
+#: application/view/share.py:105 application/view/share.py:130
+#: application/view/share.py:152
msgid "Saved to your {} account."
msgstr "已经成功被保存到你的 {} 账户。"
-#: application/view/share.py:99 application/view/share.py:117
-#: application/view/share.py:146
+#: application/view/share.py:108 application/view/share.py:126
+#: application/view/share.py:155
msgid "Failed save to {}."
msgstr "无法保存到 {}."
-#: application/view/share.py:100 application/view/share.py:118
-#: application/view/share.py:147
+#: application/view/share.py:109 application/view/share.py:127
+#: application/view/share.py:156
msgid "Reason :"
msgstr "原因:"
-#: application/view/share.py:109
+#: application/view/share.py:118
msgid "Unauthorized {} account!"
msgstr "尚未获得Pocket的授权!"
-#: application/view/share.py:122
+#: application/view/share.py:131
msgid "See details below:"
msgstr "下面是一些技术细节:"
-#: application/view/share.py:145
+#: application/view/share.py:154
msgid "Unknown: {}"
msgstr "未知: {}"
@@ -1356,41 +1361,41 @@ msgstr "标题或URL为空。"
msgid "Failed to fetch the recipe."
msgstr "抓取Recipe失败。"
-#: application/view/subscribe.py:123 application/view/subscribe.py:264
+#: application/view/subscribe.py:123 application/view/subscribe.py:265
msgid "Failed to save the recipe. Error:"
msgstr "保存Recipe失败。错误:"
-#: application/view/subscribe.py:220
+#: application/view/subscribe.py:221
msgid "You can only delete the uploaded recipe."
msgstr "您只能删除你自己上传的Recipe。"
-#: application/view/subscribe.py:224
+#: application/view/subscribe.py:225
msgid "The recipe have been subscribed, please unsubscribe it before delete."
msgstr "此Recipe已经被订阅,请先取消订阅然后再删除。"
-#: application/view/subscribe.py:237
+#: application/view/subscribe.py:238
msgid "This recipe has not been subscribed to yet."
msgstr "此Recipe尚未被订阅。"
-#: application/view/subscribe.py:251
+#: application/view/subscribe.py:252
msgid "Can not read uploaded file, Error:"
msgstr "无法读取上传的文件,错误:"
-#: application/view/subscribe.py:259
+#: application/view/subscribe.py:260
msgid ""
"Failed to decode the recipe. Please ensure that your recipe is saved in "
"utf-8 encoding."
msgstr "解码Recipe失败,请确保您的Recipe为utf-8编码。"
-#: application/view/subscribe.py:279
+#: application/view/subscribe.py:280
msgid "The recipe is already in the library."
msgstr "此Recipe已经在新闻源中。"
-#: application/view/subscribe.py:309
+#: application/view/subscribe.py:310
msgid "The login information for this recipe has been cleared."
msgstr "此Recipe的网站登录信息已经被删除。"
-#: application/view/subscribe.py:313
+#: application/view/subscribe.py:314
msgid "The login information for this recipe has been saved."
msgstr "此Recipe的网站登录信息已经保存。"
diff --git a/application/view/admin.py b/application/view/admin.py
index fbca8228..452473c6 100644
--- a/application/view/admin.py
+++ b/application/view/admin.py
@@ -86,9 +86,11 @@ def AdminAddAccountPost():
tips = _("The two new passwords are dismatch.")
elif KeUser.get_or_none(KeUser.name == username):
tips = _("Already exist the username.")
- elif not CreateAccountIfNotExist(username, password1, email,
- {'service': 'admin'} if sm_service == 'admin' else {}, expiration):
- tips = _("The password includes non-ascii chars.")
+ else:
+ sm_service = {'service': 'admin'} if sm_service == 'admin' else {}
+ sender = user.email if sm_service else email #和管理员一致则邮件发件地址也一致
+ if not CreateAccountIfNotExist(username, password1, email, sender, sm_service, expiration):
+ tips = _("The password includes non-ascii chars.")
if tips:
return render_template('user_account.html', tips=tips, formTitle=_('Add account'), submitTitle=_('Add'),
@@ -204,6 +206,22 @@ def ChangePassword(user, orgPwd, p1, p2, email):
else:
user.passwd = newPwd
user.email = email
+ if user.name == app.config['ADMIN_NAME']: #如果管理员修改email,也同步更新其他用户的发件地址
+ user.sender = email
+ SyncSenderAddress(user)
+ else: #其他人修改自己的email,根据设置确定是否要同步到发件地址
+ sm_service = user.send_mail_service
+ if not sm_service or sm_service.get('service', 'admin') != 'admin':
+ user.sender = email
+
user.save()
- tips = _("Change password success.")
+ tips = _("Changes saved successfully.")
return tips
+
+#将管理员的email同步到所有用户
+def SyncSenderAddress(adminUser):
+ for user in list(KeUser.get_all(KeUser.name != app.config['ADMIN_NAME'])):
+ sm_service = user.send_mail_service
+ if sm_service and sm_service.get('service', 'admin') == 'admin':
+ user.sender = adminUser.email
+ user.save()
diff --git a/application/view/adv.py b/application/view/adv.py
index 6dbb921c..595c503e 100644
--- a/application/view/adv.py
+++ b/application/view/adv.py
@@ -78,7 +78,6 @@ def AdvArchive():
appendStrs["Pocket"] = _("Append hyperlink '{}' to article").format(_('Save to {}').format(_('pocket')))
appendStrs["Instapaper"] = _("Append hyperlink '{}' to article").format(_('Save to {}').format(_('instapaper')))
appendStrs["Weibo"] = _("Append hyperlink '{}' to article").format(_('Share on {}').format(_('weibo')))
- appendStrs["TencentWeibo"] = _("Append hyperlink '{}' to article").format(_('Share on {}').format(_('tencent weibo')))
appendStrs["Facebook"] = _("Append hyperlink '{}' to article").format(_('Share on {}').format(_('facebook')))
appendStrs["X"] = _("Append hyperlink '{}' to article").format(_('Share on {}').format('X'))
appendStrs["Tumblr"] = _("Append hyperlink '{}' to article").format(_('Share on {}').format(_('tumblr')))
@@ -87,7 +86,8 @@ def AdvArchive():
shareLinks.pop('key', None)
return render_template('adv_archive.html', tab='advset', user=user, advCurr='archive', appendStrs=appendStrs,
- shareLinks=shareLinks, in_email_service=app.config['INBOUND_EMAIL_SERVICE'])
+ shareLinks=shareLinks, in_email_service=app.config['INBOUND_EMAIL_SERVICE'],
+ ke_decrypt=ke_decrypt)
@bpAdv.post("/adv/archive", endpoint='AdvArchivePost')
@login_required()
@@ -117,7 +117,6 @@ def AdvArchivePost():
shareLinks['Pocket'] = {'enable': '1' if pocket else '', 'access_token': accessToken}
shareLinks['Instapaper'] = {'enable': '1' if instapaper else '', 'username': instaName, 'password': instaPwd}
shareLinks['Weibo'] = str_to_bool(form.get('weibo'))
- shareLinks['TencentWeibo'] = str_to_bool(form.get('tencentweibo'))
shareLinks['Facebook'] = str_to_bool(form.get('facebook'))
shareLinks['X'] = str_to_bool(form.get('x'))
shareLinks['Tumblr'] = str_to_bool(form.get('tumblr'))
@@ -349,14 +348,14 @@ def AdvOAuth2(authType):
return 'Auth Type ({}) Unsupported!'.format(authType)
user = get_login_user()
- cbUrl = urljoin(app.config['APP_DOMAIN'], '/oauth2cb/pocket?redirect=/adv/archive')
+ cbUrl = urljoin(app.config['APP_DOMAIN'], '/oauth2cb/pocket?redirect={}'.format(url_for("bpAdv.AdvArchive")))
pocket = Pocket(app.config['POCKET_CONSUMER_KEY'], cbUrl)
try:
request_token = pocket.get_request_token()
url = pocket.get_authorize_url(request_token)
except Exception as e:
return render_template('tipsback.html', title='Authorization Error', urltoback=url_for('bpAdv.AdvArchive'),
- tips=_('Authorization Error! {}').format(e))
+ tips=_('Authorization Error! {}').format(str(e)))
session['pocket_request_token'] = request_token
return redirect(url)
@@ -370,18 +369,18 @@ def AdvOAuth2Callback(authType):
user = get_login_user()
- pocketInst = Pocket(app.config['POCKET_CONSUMER_KEY'])
+ pocket = Pocket(app.config['POCKET_CONSUMER_KEY'])
request_token = session.get('pocket_request_token', '')
shareLinks = user.share_links
try:
- resp = pocketInst.get_access_token(request_token)
- pocket = shareLinks.get('pocket', {})
- pocket['access_token'] = resp.get('access_token', '')
+ acToken = pocket.get_access_token(request_token)
+ shareLinks.setdefault('pocket', {})
+ shareLinks['pocket']['access_token'] = acToken
user.share_links = shareLinks
user.save()
return render_template('tipsback.html', title='Success authorized', urltoback=url_for('bpAdv.AdvArchive'), tips=_('Success authorized by Pocket!'))
except Exception as e:
- shareLinks[pocket] = {'enable': '', 'access_token': ''}
+ shareLinks['pocket'] = {'enable': '', 'access_token': ''}
user.share_links = shareLinks
user.save()
return render_template('tipsback.html', title='Failed to authorize', urltoback=url_for('bpAdv.AdvArchive'),
diff --git a/application/view/inbound_email.py b/application/view/inbound_email.py
index a36c18f4..2f2f93d4 100644
--- a/application/view/inbound_email.py
+++ b/application/view/inbound_email.py
@@ -72,7 +72,7 @@ def IsSpamMail(sender, user):
@bpInBoundEmail.post("/_ah/bounce")
def ReceiveBounce():
msg = mail.BounceNotification(dict(request.form.lists()))
- #default_log.warning("Bounce original: {}, notification: {}".format(msg.original, msg.notification))
+ default_log.warning("Bounce original: {}, notification: {}".format(msg.original, msg.notification))
return "OK", 200
#有新的邮件到达, _ah=apphosting
diff --git a/application/view/login.py b/application/view/login.py
index 0f55798d..8bb3d5b8 100644
--- a/application/view/login.py
+++ b/application/view/login.py
@@ -25,9 +25,6 @@ def Login():
if CreateAccountIfNotExist(adminName):
tips = (_("Please use {}/{} to login at first time.").format(adminName, adminName))
- session['login'] = 0
- session['userName'] = ''
- session['role'] = ''
return render_template('login.html', tips=tips)
@bpLogin.post("/login")
@@ -75,34 +72,39 @@ def LoginPost():
tips = (_("The username does not exist or password is wrong.") +
f'' + _('Forgot password?') + ' ')
- session['login'] = 0
- session['userName'] = ''
- session['role'] = ''
+ session.pop('login', None)
+ session.pop('userName', None)
+ session.pop('role', None)
return render_template('login.html', userName=name, tips=tips)
#判断账号是否存在
#如果账号不存在,创建一个,并返回True,否则返回False
-def CreateAccountIfNotExist(name, password=None, email=None, sm_service=None, expiration=0):
+def CreateAccountIfNotExist(name, password=None, email='', sender=None, sm_service=None, expiration=0):
if KeUser.get_or_none(KeUser.name == name):
return False
password = password if password else name
- email = email if email else app.config['SRC_EMAIL']
secretKey = new_secret_key()
shareKey = new_secret_key(length=4)
try:
password = hashlib.md5((password + secretKey).encode()).hexdigest()
except Exception as e:
- default_log.warning('CreateAccountIfNotExist() failed to hash password: {}'.format(str(e)))
+ default_log.warning('CreateAccountIfNotExist failed to hash password: {}'.format(str(e)))
return False
+ adminName = app.config['ADMIN_NAME']
if sm_service is None:
sm_service = {}
- if name != app.config['ADMIN_NAME'] and AppInfo.get_value(AppInfo.newUserMailService, 'admin') == 'admin':
+ if name != adminName and AppInfo.get_value(AppInfo.newUserMailService, 'admin') == 'admin':
sm_service = {'service': 'admin'}
-
- user = KeUser(name=name, passwd=password, timezone=app.config['TIMEZONE'], expires=None, secret_key=secretKey,
- expiration_days=expiration, share_links={'key': shareKey}, email=email, send_mail_service=sm_service)
+ if sender is None:
+ adUser = KeUser.get_or_none(KeUser.name == adminName)
+ sender = adUser.email if adUser else email
+
+ sender = sender or email
+
+ user = KeUser(name=name, passwd=password, expires=None, secret_key=secretKey, expiration_days=expiration,
+ share_links={'key': shareKey}, email=email, sender=sender, send_mail_service=sm_service)
if expiration:
user.expires = datetime.datetime.utcnow() + datetime.timedelta(days=expiration)
user.save()
@@ -110,9 +112,9 @@ def CreateAccountIfNotExist(name, password=None, email=None, sm_service=None, ex
@bpLogin.route("/logout", methods=['GET', 'POST'])
def Logout():
- session['login'] = 0
- session['userName'] = ''
- session['role'] = ''
+ session.pop('login', None)
+ session.pop('userName', None)
+ session.pop('role', None)
return redirect('/')
#for ajax parser, if login required, retuan a dict
diff --git a/application/view/setting.py b/application/view/setting.py
index c5571315..25e48d74 100644
--- a/application/view/setting.py
+++ b/application/view/setting.py
@@ -20,8 +20,7 @@
def Setting(tips=None):
user = get_login_user()
sm_services = avaliable_sm_services()
- return render_template('setting.html', tab='set', user=user, tips=tips, src_mail=app.config['SRC_EMAIL'],
- langMap=LangMap(), sm_services=sm_services)
+ return render_template('setting.html', tab='set', user=user, tips=tips, langMap=LangMap(), sm_services=sm_services)
@bpSetting.post("/setting", endpoint='SettingPost')
@login_required()
@@ -38,13 +37,14 @@ def SettingPost():
sm_srv_need = True
sm_srv_type = form.get('sm_service')
sm_apikey = form.get('sm_apikey', '')
+ sm_secret_key = form.get('sm_secret_key', '')
sm_host = form.get('sm_host', '')
sm_port = str_to_int(form.get('sm_port'))
- sm_username = form.get('sm_username', '')
+ #sm_username = form.get('sm_username', '') #replace by sender field
sm_password = form.get('sm_password', '')
sm_save_path = form.get('sm_save_path', '')
- send_mail_service = {'service': sm_srv_type, 'apikey': sm_apikey, 'host': sm_host,
- 'port': sm_port, 'username': sm_username, 'password': '',
+ send_mail_service = {'service': sm_srv_type, 'apikey': sm_apikey, 'secret_key': sm_secret_key,
+ 'host': sm_host, 'port': sm_port, 'username': '', 'password': '',
'save_path': sm_save_path}
#只有处于smtp模式并且密码存在才更新,空或几个星号则不更新
if sm_srv_type == 'smtp':
@@ -59,7 +59,7 @@ def SettingPost():
tips = _("Title is requied!")
elif sm_srv_type == 'sendgrid' and not sm_apikey:
tips = _("Some parameters are missing or wrong.")
- elif sm_srv_type == 'smtp' and not all((sm_host, sm_port, sm_username, sm_password)):
+ elif sm_srv_type == 'smtp' and not all((sm_host, sm_port, sm_password)):
tips = _("Some parameters are missing or wrong.")
elif sm_srv_type == 'local' and not sm_save_path:
tips = _("Some parameters are missing or wrong.")
@@ -76,7 +76,7 @@ def SettingPost():
user.enable_custom_rss = False
user.kindle_email = keMail
- user.timezone = int(form.get('timezone', app.config['TIMEZONE']))
+ user.timezone = int(form.get('timezone', '0'))
user.send_time = int(form.get('send_time', '0'))
user.book_type = form.get('book_type', 'epub')
user.device = form.get('device_type', 'kindle')
@@ -99,8 +99,7 @@ def SettingPost():
UpdateBookedCustomRss(user)
sm_services = avaliable_sm_services()
- return render_template('setting.html', tab='set', user=user, tips=tips, src_mail=app.config['SRC_EMAIL'],
- langMap=LangMap(), sm_services=sm_services)
+ return render_template('setting.html', tab='set', user=user, tips=tips, langMap=LangMap(), sm_services=sm_services)
#设置国际化语种
@bpSetting.route("/setlocale/")
diff --git a/application/view/share.py b/application/view/share.py
index d01d0023..ed64bc73 100644
--- a/application/view/share.py
+++ b/application/view/share.py
@@ -7,6 +7,7 @@
from flask import Blueprint, render_template, request, current_app as app
from flask_babel import gettext as _
from calibre.web.feeds.news import recursive_fetch_url
+import readability
from ..base_handler import *
from ..back_end.db_models import *
from ..back_end.send_mail_adpt import send_html_mail
@@ -59,7 +60,15 @@ def SaveToEvernoteWiz(user, action, url, title):
fs = FsDictStub(None)
res, paths, failures = recursive_fetch_url(url, fs)
if res:
- soup = BeautifulSoup(fs.read(res), 'lxml')
+ raw = fs.read(res)
+ positives = ['image-block', 'image-block-caption', 'image-block-ins']
+ try:
+ doc = readability.Document(raw, positive_keywords=positives, url=url)
+ summary = doc.summary(html_partial=False)
+ except:
+ summary = raw
+
+ soup = BeautifulSoup(summary, 'lxml')
p = soup.new_tag('p', style='font-size:80%;color:grey;') #插入源链接
a = soup.new_tag('a', href=url)
a.string = url
diff --git a/application/view/subscribe.py b/application/view/subscribe.py
index b5bfaaa4..f528ee67 100644
--- a/application/view/subscribe.py
+++ b/application/view/subscribe.py
@@ -161,7 +161,8 @@ def UpdateBookedCustomRss(user: KeUser):
'title': rss.title, 'description': rss.description, 'time': datetime.datetime.utcnow()})
else: #删除订阅
ids = [rss.recipe_id for rss in user.all_custom_rss()]
- BookedRecipe.delete().where(BookedRecipe.recipe_id.in_(ids)).execute()
+ if ids:
+ BookedRecipe.delete().where(BookedRecipe.recipe_id.in_(ids)).execute()
#通知共享服务器,有一个新的订阅
def SendNewSubscription(title, url, recipeId):
diff --git a/application/work/url2book.py b/application/work/url2book.py
index e3a178ed..9088eb30 100644
--- a/application/work/url2book.py
+++ b/application/work/url2book.py
@@ -69,7 +69,7 @@ def Url2BookImpl(userName: str, urls: str, subject: str, action: str):
resp = opener.open(url)
if resp.status_code == 200:
attachments = [('page.html', resp.content)]
- send_mail(user, app.config['SRC_EMAIL'], 'DEBUG FETCH', 'DEBUG FETCH', attachments=attachments)
+ send_mail(user, user.email, 'DEBUG FETCH', 'DEBUG FETCH', attachments=attachments)
else:
default_log.warning('debug_fetch failed: code:{}, url:{}'.format(resp.status_code, url))
diff --git a/application/work/worker.py b/application/work/worker.py
index 97a3e372..6aa106a2 100644
--- a/application/work/worker.py
+++ b/application/work/worker.py
@@ -44,8 +44,7 @@ def WorkerImpl(userName: str, recipeId: list=None, log=None):
return f"The user '{userName}' does not exist."
if not log:
- log = logging.getLogger('WorkerImpl')
- log.setLevel(logging.WARN)
+ log = default_log
if not recipeId:
recipeId = [item.recipe_id for item in user.get_booked_recipe()]
diff --git a/config.py b/config.py
index 838f0ba8..5f0f29aa 100644
--- a/config.py
+++ b/config.py
@@ -2,30 +2,29 @@
# -*- coding:utf-8 -*-
"""For KindleEar configuration, the first several variables need to be modified.
If some configurations need to be obtained through environment variables,
-you can use the form os.environ['name'] (import os firstly)
-
-KindleEar配置文件,请务必修改开始几个配置
-如果有的配置是从环境变量获取,也可以使用os.envrion['name']方式。(在开头增加一行: import os)
+you can use the form os.environ['name']
"""
+import os
-APP_ID = "kindleear"
-SRC_EMAIL = "akindleear@gmail.com" #Your gmail account for sending mail to Kindle
-APP_DOMAIN = "https://kindleear.appspot.com" #Your domain of app
+APP_ID = os.getenv("APP_ID") or "kindleear"
+APP_DOMAIN = os.getenv("APP_DOMAIN") or "https://kindleear.appspot.com"
#Need for google taskqueue only, Refers to
#Find it at Upper right corner of
+#Or get by cmd: gcloud beta app describe
+#Two exceptions: europe-west should be europe-west1, us-central should be us-central1
SERVER_LOCATION = "us-central1"
#Choose the database
#Supports: "datastore", "sqlite", "mysql", "postgresql", "cockroachdb", "mongodb", "redis", "pickle"
#DATABASE_URL = "mongodb://127.0.0.1:27017/"
-DATABASE_URL = 'pickle:////D:/Programer/Project/KindleEar/test.pkl'
-#DATABASE_URL = 'redis://127.0.0.1:6379/0'
+#DATABASE_URL = 'sqlite:////home/ubuntu/site/kindleear/database.db'
+DATABASE_URL = 'sqlite:////d:/programer/project/kindleear/database.db'
#Email receiving service, "gae", ""
INBOUND_EMAIL_SERVICE = ""
-#Select the type of task queue, "gae", "apscheduler", "celery", "rq"
+#Select the type of task queue, "gae", "apscheduler", "celery", "rq", ""
TASK_QUEUE_SERVICE = "apscheduler"
#If task queue service is apscheduler, celery, rq
@@ -36,13 +35,16 @@
#If this option is empty, temporary files will be stored in memory
#Setting this option can reduce memory consumption, supports both relative and absolute paths
-TEMP_DIR = ""
+TEMP_DIR = os.getenv("TEMP_DIR") or "/tmp"
#If the depolyment plataform supports multi-threads, set this option will boost the download speed
DOWNLOAD_THREAD_NUM = 1
#If the website allow visitors to signup or not
-ALLOW_SIGNUP = True
+ALLOW_SIGNUP = False
+
+#For security reasons, it's suggested to change the secret key.
+SECRET_KEY = "n7ro8QJI1qff"
#------------------------------------------------------------------------------------
#Configurations below this line generally do not need to be modified
@@ -51,10 +53,8 @@
#The administrator's login name
ADMIN_NAME = "admin"
-TIMEZONE = 8 #Default timezone, you can modify it in webpage after deployed
-
#You can use this public key or apply for your own key
-POCKET_CONSUMER_KEY = '50188-e221424f1c9ed0c010058aef'
+POCKET_CONSUMER_KEY = "50188-e221424f1c9ed0c010058aef"
#Hide the option 'local (debug)' of 'Send Mail Service' setting or not
HIDE_MAIL_TO_LOCAL = False
diff --git a/docs/Chinese/1.intro.md b/docs/Chinese/1.intro.md
index 606051fb..4f2c80dd 100644
--- a/docs/Chinese/1.intro.md
+++ b/docs/Chinese/1.intro.md
@@ -3,6 +3,6 @@ sort: 1
---
# Intro
-KindleEar是一个开源免费网络应用,可以部署在大多数支持Python的托管平台,包括但不限于google cloud、Heroku、Pythonanywhere、VPS、ubuntu、树莓派等,主要功能是自动定期通过RSS收集网络文章然后制作成图文并茂的电子书推送至你的Kindle或其他电子阅读设备。
+KindleEar是一个开源免费网络应用,可以部署在大多数支持Python的托管平台,包括但不限于Google Cloud、Heroku、VPS、Ubuntu、树莓派等,主要功能是自动定期通过RSS收集网络文章然后制作成图文并茂的电子书推送至你的Kindle或其他电子阅读设备。
Kindle修改和提取了著名电子书管理软件Calibre的epub/mobi生成模块,除了直接输入RSS链接地址即可推送外,还直接支持Calibre的Recipe格式(抓取各种网站信息的Python脚本),已经内置一千多个Recipe,涵盖多种语种,除此之外,您也可以编写自己的Recipe然后在管理页面上传到KindleEar。
diff --git a/docs/Chinese/2.deployment.md b/docs/Chinese/2.config.md
similarity index 76%
rename from docs/Chinese/2.deployment.md
rename to docs/Chinese/2.config.md
index a1e87b08..1c344efa 100644
--- a/docs/Chinese/2.deployment.md
+++ b/docs/Chinese/2.config.md
@@ -1,22 +1,21 @@
---
sort: 2
---
-# 部署步骤
+# 配置项描述
-
-## config.py
+
+## 基本配置项
不管部署到什么平台,都先要正确配置config.py,这一节描述几个简单的配置项,其他配置项在下面章节详细描述。
| 配置项 | 含义 |
| ------------------- | -------------------------------------------------------- |
| APP_ID | 应用标识符,gae平台为应用ID,其他平台用于标识数据库等资源 |
-| SRC_EMAIL | 发送邮件的发件人地址 |
| APP_DOMAIN | 应用部署后的域名 |
| TEMP_DIR | 制作电子书时的临时目录,为空则使用内存保存临时文件 |
| DOWNLOAD_THREAD_NUM | 下载网页的线程数量,需要目标平台支持多线程,最大值为5 |
| ALLOW_SIGNUP | 是否允许用户注册,False为需要管理员创建账号 |
+| SECRET_KEY | 浏览器session的加密密钥,建议修改,任意字符串即可 |
| ADMIN_NAME | 管理员的账号名 |
-| TIMEZONE | 新账号的默认时区,账号被创建后还可以在网页上自己修改时区 |
| POCKET_CONSUMER_KEY | 用于稍后阅读服务Pocket,可以用你自己的Key或就直接使用这个 |
| HIDE_MAIL_TO_LOCAL | 是否允许将生成的邮件保存到本地,用于调试或测试目的 |
@@ -27,7 +26,8 @@ sort: 2
## 数据库选择
数据库用于保存应用的配置数据和订阅数据。
得益于SQL数据库ORM库 [peewee](https://pypi.org/project/peewee/) 和作者因KindleEar需要而创建的NoSQL数据库ODM库 [weedata](https://pypi.org/project/weedata/),KindleEar支持很多数据库类型,包括:datastore, sqlite, mysql, postgresql, cockroachdb, mongodb, redis, pickle, 基本兼容了市面上的主流数据库,更适合全平台部署,平台支持什么数据库就可以使用什么数据库。
-本应用的数据量不大,确切的来说,很小很小,一般就几十行数据,选择什么数据库都不会对资源消耗和性能造成什么影响,即使最简单的就用一个文本文件当做数据库使用可能都会比其他正规的数据库要快。
+如果目标平台同时支持SQL/NoSQL,则建议NoSQL,其最大的优点是万一以后升级需要修改数据库结构,则NoSQL不会影响原有数据,而SQL会删掉原数据。
+本应用的数据量不大,确切的来说,很小很小,一般就几十行数据,选择什么数据库都不会对资源消耗和性能造成什么影响,即使最简单的就用一个文本文件当做数据库使用可能都会比其他正规的数据库要快。
@@ -35,7 +35,7 @@ sort: 2
Datastore为google的NoSQL数据库,我们要使用的是firebase的datastore模式。
如果要部署到google cloud,基本上你只能选择datastore,因为它有免费额度。
要使用datastore,参数配置如下:
-```
+```python
DATABASE_URL = 'datastore'
```
@@ -44,7 +44,7 @@ DATABASE_URL = 'datastore'
### SQLite
单文件数据库。适用于有本地文件系统读写权限的平台,特别是资源受限系统比如树莓派和各种派之类的。
要使用datastore,参数配置如下:
-```
+```python
#template:
DATABASE_URL = 'sqlite:////path/to/database.db'
#examples:
@@ -57,7 +57,7 @@ DATABASE_URL = 'sqlite:////home/username/dbfilename.db'
### MySQL/PostgreSQL/CockroachDB
典型企业级SQL数据库。大炮打蚊子,如果平台支持,直接使用也无妨。
参数配置如下:
-```
+```python
#template:
DATABASE_URL = 'mysql://username:password@hostname:port/database_name'
DATABASE_URL = 'postgresql://username:password@hostname:port/database_name'
@@ -82,7 +82,7 @@ database_url = f"mysql://{db_username}:{db_password}@{db_host}:{db_port}/{db_nam
### MongoDB
应用最广的典型NoSQL数据库。
参数配置如下:
-```
+```python
#template:
DATABASE_URL = 'mongodb://username:password@hostname:port/'
#examples:
@@ -93,20 +93,20 @@ DATABASE_URL = 'mongodb://user:pass123@example.com:27017/'
### Redis
-可以持久化到磁盘的内存数据库。如果目标系统已经安装并使用了redis用于任务队列,则直接使用redis可以省去安装其他数据库的资源消耗,但使用前要做好相关的redis持久化配置,避免丢失数据。
+可以持久化到磁盘的NoSQL内存数据库。如果目标系统已经安装并使用了redis用于任务队列,则直接使用redis可以省去安装其他数据库的资源消耗,但使用前要做好相关的redis持久化配置,避免丢失数据。
参数配置如下(db_number可以省略,如果是0,建议省略):
-```
+```python
DATABASE_URL = 'redis://[:password]@hostname:port/db_number'
-DATABASE_URL = 'redis://127.0.0.1:6379/0'
+DATABASE_URL = 'redis://127.0.0.1:6379/'
DATABASE_URL = 'redis://:password123@example.com:6379/1'
```
### Pickle
-作者使用Python的pickle数据持久化标准库创建的一个非常简单的单文件"数据库",可以用于资源特别受限的系统或用于测试目的。
+作者使用Python的pickle数据持久化标准库创建的一个非常简单的单文件NoSQL"数据库",可以用于资源特别受限的系统或用于测试目的。
参数配置如下:
-```
+```python
#template:
DATABASE_URL = 'pickle:////path/to/database.db'
#examples:
@@ -123,7 +123,7 @@ DATABASE_URL = 'pickle:////home/username/dbfilename.db'
### gae
如果要部署到google cloud,你只能选择gae。
-```
+```python
TASK_QUEUE_SERVICE = "gae"
TASK_QUEUE_BROKER_URL = ""
```
@@ -133,7 +133,7 @@ TASK_QUEUE_BROKER_URL = ""
### apscheduler
比较轻量,最简配置可以不依赖redis和其他数据库,直接使用内存保存任务状态,只是有一定的丢失任务风险,在任务队列执行过程中掉电重新上电后原任务不会重新运行,只能等新的任务时间到达。
如果要使用数据库持久化,支持sqlite/mysql/postgresql/mongodb/redis,可以配置为DATABASE_URL相同的值。
-```
+```python
TASK_QUEUE_SERVICE = "apscheduler"
TASK_QUEUE_BROKER_URL = "" #use memory store
@@ -147,7 +147,7 @@ TASK_QUEUE_BROKER_URL = "sqlite:////home/username/dbfilename.db"
### celery
最著名的任务队列,支持多种后端技术,包括redis、mongodb、sql、共享目录等,如果要使用数据库保存任务状态,可以配置为DATABASE_URL相同的值。
-```
+```python
TASK_QUEUE_SERVICE = "celery"
TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
@@ -162,7 +162,7 @@ TASK_QUEUE_BROKER_URL = "file:////?/C:/Users/name/results/" #keep the prefix 'fi
### rq
比celery稍轻量的任务队列,依赖redis,需要额外安装redis服务。
-```
+```python
TASK_QUEUE_SERVICE = "celery"
TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
```
@@ -172,9 +172,20 @@ TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
## 邮件发送服务选择
为了更方便使用和规避一些免费额度的限制,邮件发送服务可以等部署完成后在网页上配置。
-* **gae**: 部署到google cloud时建议使用,额度足够,邮件大小也慷慨,单邮件最大31.5MB。
-* **sendgrid**: 部署到其他平台时建议使用,需要额外 [注册账号](https://sendgrid.com/) 并申请一个ApiKey,单邮件最大30MB。
-* **SMTP**: 这个选项就灵活了,大部分的电子邮件服务平台都支持SMTP,ubuntu等平台也很方便的部署一个自己的SMTP服务。
+* **gae**:
+部署到google cloud时建议使用,额度足够,邮件大小也慷慨,单邮件最大31.5MB。
+
+* **sendgrid**:
+部署到其他平台时建议使用,需要额外 [注册账号](https://sendgrid.com/) 并申请一个ApiKey,单邮件最大30MB。
+注:我一直无法正常注册sendgrid,不管换多少种方法多少个邮件地址都无法正常登录sendgrid,所以此功能我没有亲自测试,测试通过的朋友可以告知我。
+
+* **mailjet**:
+[Mailjet](https://www.mailjet.com/)是另一个选项, 需要注册账号并且申请ApiKey和SecretKey,单邮件最大15MB。记得发送前确认自己的发件地址有没有在 [Sender addresses](https://app.mailjet.com/account/sender) 里面。
+测试过程中还发现一个问题,如果你的发件人邮件地址不是你注册mailjet的地址,则mailjet不会报错,只是对方永远收不到。所以如果使用mailjet发送邮件失败,请确认发件地址是否正确。
+
+* **SMTP**:
+这个选项就灵活了,大部分的电子邮件服务平台都支持SMTP,只是很多平台对SMTP登录都有诸多限制,使用前请注意阅读相关说明,特别是大多数平台的SMTP密码都和正常的账号密码不一致。
+除了使用市场上已有的服务外,ubuntu等平台也很方便的部署一个自己的SMTP服务。
@@ -182,14 +193,14 @@ TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
## 邮件接收服务
gae平台有一个比较独特的服务,应用部署完成后除了可以发送邮件,还可以接收邮件,这个功能有时候还是比较有用的,具体的用法可以参考 [FAQ](3.faq.html#appspotmail)。
如果要启用此服务,配置如下(注意:仅gae平台支持,其他平台设置了也没用):
-```
+```python
INBOUND_EMAIL_SERVICE = 'gae'
```
-
-## Web 服务器
+
+## WSGI协议容器
KindleEar使用Flask框架实现Web界面管理,入口点在 main.app (main.py文件内的app实例对象),
可以使用任何支持wsgi标准接口的Web服务器软件启动此app即可。
要求不高的,使用Flask的调试服务器直接启动都可以。
@@ -201,7 +212,7 @@ KindleEar使用Flask框架实现Web界面管理,入口点在 main.app (main.
## requirements.txt
KindleEar使用requirements.txt管理各种库的依赖,在各种平台部署时都可以一行命令就完成应用的环境配置
-```
+```bash
pip install -r requirments.txt
```
因各种配置组合较多,要手动配置requirements.txt比较复杂,容易出错。
@@ -210,19 +221,3 @@ pip install -r requirments.txt
或者你可以不用此脚本,而是将requirements.txt里面的所有注释都删除,也就是安装全部依赖库,反正也占用不了多少空间。
-
-
-## google cloud
-1. github页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如D:\KindleEar。
-2. 安装标准环境google cloud SDK/gloud CLI,并且执行
-```
-gcloud init
-gcloud auth login
-gcloud config set project your_app_id
-gcloud app deploy --version=1 path\to\app.yaml
-gcloud app deploy --version=1 path\to\kindleear
-gcloud app deploy --version=1 path\to\cron.yaml
-gcloud app deploy --version=1 path\to\queue.yaml
-gcloud datastore indexes create path\to\index.yaml
-```
-
diff --git a/docs/Chinese/3.deployment.md b/docs/Chinese/3.deployment.md
new file mode 100644
index 00000000..b82c3159
--- /dev/null
+++ b/docs/Chinese/3.deployment.md
@@ -0,0 +1,160 @@
+---
+sort: 3
+---
+# 部署方法
+KindleEar支持多种平台部署,我只在这里列出一些我测试通过的平台,欢迎补充其他平台的部署方法。
+
+
+
+## google cloud (PaaS)
+1. config.py关键参数样例
+```python
+SERVER_LOCATION = "us-central1"
+DATABASE_URL = "datastore"
+INBOUND_EMAIL_SERVICE = "gae"
+TASK_QUEUE_SERVICE = "gae"
+TASK_QUEUE_BROKER_URL = ""
+```
+
+2. github页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如D:\KindleEar。
+
+3. 安装 [gloud CLI](https://cloud.google.com/sdk/docs/install),并且执行
+```bash
+gcloud components install app-engine-python app-engine-python-extras # Run as Administrator
+gcloud init
+gcloud auth login
+gcloud auth application-default set-quota-project your_app_id
+gcloud config set project your_app_id
+gcloud beta app deploy --version=1 app.yaml
+gcloud beta app deploy --version=1 cron.yaml
+gcloud beta app deploy --version=1 queue.yaml
+```
+
+4. 如果出现部署失败并且多次尝试后仍然无法解决,比如"Timed out fetching pod."之类的错误,可以关停此id,然后重建一个,部署时选择其他区域。
+
+5. 部署成功后先到 [GAE后台](https://console.cloud.google.com/appengine/settings/emailsenders) 将你的发件地址添加到 "Mail API Authorized Senders",否则投递会出现 "Unauthorized sender" 错误。
+
+
+
+
+## Oracle cloud (VPS)
+1. config.py关键参数样例
+```python
+DATABASE_URL = "sqlite:////home/ubuntu/site/kindleear/database.db"
+INBOUND_EMAIL_SERVICE = ""
+TASK_QUEUE_SERVICE = "apscheduler"
+TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
+TEMP_DIR = "/tmp"
+DOWNLOAD_THREAD_NUM = 1
+```
+
+2. 创建一个计算实例,选择的配置建议"符合始终免费条件",映像选择自己熟悉的,我选择的是ubuntu minimal。
+记得下载和保存私钥。
+创建完成后在"实例信息"点击"子网"链接,在"安全列表"中修改或创建入站规则,将TCP的端口删除,ICMP的类型和代码删除,然后测试ping对应的IP,能ping通说明实例配置完成。
+
+3. 使用自己喜欢的SSH工具远程连接对应IP。
+3.1 如果使用puTTY,需要先使用puttyGen将key格式的私钥转换为ppk格式。
+打开puTTY,Host格式为username@IP,端口号22,用户名在"实例信息"中可以找到,在Connection|SSH|Auth|Credentials导入私钥文件。
+3.2 如果使用Xshell,身份验证选择Public Key,并导入之前保存的私钥文件。
+
+4. 登录进去后建议先修改root密码
+```bash
+sudo -i
+passwd
+```
+
+
+5. 然后就是命令时间
+```bash
+sudo apt update
+sudo apt upgrade
+sudo apt install nginx
+sudo apt install git python3.10 python3-pip
+sudo pip3 install virtualenv
+sudo apt install redis-server
+sudo systemctl start nginx
+sudo systemctl start redis-server
+sudo systemctl enable nginx.service
+sudo systemctl enable redis-server.service
+
+curl localhost #test if nginx works well
+
+sudo apt install vim-common
+mkdir ~/site
+mkdir ~/log
+cd ~/site
+
+#fetch code from github, or you can upload code files by xftp/scp
+git clone --depth 1 https://github.com/cdhigh/kindleear.git
+chmod -R 775 ~ #nginx user www-data read static resource
+sudo usermod -aG ubuntu www-data #or add nginx www-data to my group ubuntu
+cd kindleear
+virtualenv --python=python3 venv #create virtual environ
+vim ./config.py #start to modify some config items
+python3 ./tools/deploy_helper.py #update requirements.txt
+
+source ./venv/bin/activate #activate virtual environ
+pip install -r requirements.txt #install dependencies
+python3 ./main.py db create #create database tables
+
+#open port 80/443
+sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
+sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT
+sudo netfilter-persistent save
+
+#modify nginx configuration
+vim ./tools/nginx/default #optional, change server_name if you want
+sudo ln -f ./tools/nginx/default /etc/nginx/sites-enabled/default
+sudo nginx -t #test if nginx config file is correct
+
+#set gunicorn auto start
+sudo cp ./tools/nginx/gunicorn.service /usr/lib/systemd/system
+sudo systemctl daemon-reload
+sudo systemctl start gunicorn.service
+sudo systemctl status gunicorn.service
+sudo systemctl enable gunicorn.service
+
+sudo systemctl restart nginx.service
+sudo systemctl status nginx.service
+```
+
+6. 现在你就可以在浏览器中使用 "http://ip" 来确认是否已经部署成功。如果愿意,也可以继续配置nginx来使用ssl,如果已有域名,也可以绑定自己的域名。
+
+7. 出现错误后,查询后台log的命令
+```bash
+cat /var/log/nginx/error.log
+cat /home/ubuntu/log/gunicorn.error.log
+cat /home/ubuntu/log/gunicorn.access.log
+```
+
+8. 后语,如果部署在Oracle cloud,建议开启其"OCI Email Delivery"服务,然后使用SMTP发送邮件,单邮件最大支持60MB,我还没有发现有哪家服务商能支持那么大的邮件。
+
+
+
+
+
+## PythonAnywhere (PaaS)
+1. config.py关键参数样例
+```python
+DATABASE_URL = "mysql://name:pass@name.mysql.pythonanywhere-services.com/name$default"
+INBOUND_EMAIL_SERVICE = ""
+TASK_QUEUE_SERVICE = ""
+TASK_QUEUE_BROKER_URL = ""
+```
+
+2. 登录 [pythonanywhere](https://www.pythonanywhere.com),转到 "Web" 选项卡,点击左侧 "Add a new web app",创建一个Flask应用。
+
+3. 转到 "Databases" 选项卡,初始化mysql并创建一个数据库。
+
+4. 参考 [UploadingAndDownloadingFiles](https://help.pythonanywhere.com/pages/UploadingAndDownloadingFiles) 文档,使用git或zip方法上传代码。
+5. 在 "Files" 选项卡打开一个 Bash console,执行bash命令 `pip install -r requirements.txt`
+
+6. 创建定时任务。PythonAnywhere不支持代码中自由设置定时任务,并且免费用户只能设置一个定时时间,限制较大,不过如果要勉强使用,可以到 "Tasks" 选项卡,根据你希望推送订阅的时间创建一个Task,命令行为:
+`python /home/yourname/yourdirectory/main.py deliver now`
+如果部署在PythonAnywhere,则网页上的投递时间设置无效,投递时间就是这个Task的执行时间。
+
+7. 如果你是免费用户,需要至少每三个月登录一次pythonanywhere,点击一次 "Run until 3 months from today",否则你的应用就会被暂停。
+
+注:经过测试,除非付费,否则PythonAnywhere不适合我们的应用部署,因为其限制较多,最致命的限制就是其对免费用户能访问的网站实施白名单措施,不在其 [列表中的网站](https://www.pythonanywhere.com/whitelist/) 无法访问。
+
+
diff --git a/docs/Chinese/3.faq.md b/docs/Chinese/4.faq.md
similarity index 95%
rename from docs/Chinese/3.faq.md
rename to docs/Chinese/4.faq.md
index 80a1f8a8..6edddae5 100644
--- a/docs/Chinese/3.faq.md
+++ b/docs/Chinese/4.faq.md
@@ -1,5 +1,5 @@
---
-sort: 3
+sort: 4
---
# FAQ
@@ -31,7 +31,7 @@ cover_url = 'https://www.google.com/mycover.jpg'
## 忘记密码了怎么办?
-KindleEar不保存密码原文,无法取回密码。在登录时密码验证错误时会有一个“忘记密码?”链接,点击这个链接就可以使用创建账号时登记的email邮箱来重置密码,管理员的重置密码email邮箱就是config.py里面的SRC_EMAIL。
+KindleEar不保存密码原文,无法取回密码。在登录时密码验证错误时会有一个“忘记密码?”链接,点击这个链接就可以使用创建账号时登记的email邮箱来重置密码。
@@ -65,9 +65,6 @@ needs_subscription = True
“订阅”是合并推送,将所有按这个选项订阅的Recipe和自定义RSS都合并为一个文件推送,“订阅(单独推送)”是将这个Recipe单独创建一个文件推送,更适合一些生成文件比较大或有特殊推送时间的Recipe。
-## 投递日志状态显示 wrong SRC_EMAIL?
-到 [GAE后台](https://console.cloud.google.com/appengine) 的Settings页面,看看 已经授权的Email列表里面有没有你的发送邮箱地址,如果没有就添加即可。
-
## 我还有更多问题,到哪里去问?
如果你碰到更多问题,可以到 [https://github.com/cdhigh/KindleEar/issues](https://github.com/cdhigh/KindleEar/issues) 去提交一个issue,然后等待答复,不过在提交问题之前,还是建议先搜索一下别人先前提交的issues,说不定已经有人重复提过了相关问题呢?
diff --git a/docs/Chinese/README.md b/docs/Chinese/README.md
index 13a7a317..50ba7379 100644
--- a/docs/Chinese/README.md
+++ b/docs/Chinese/README.md
@@ -4,13 +4,13 @@ sort: 2
# 中文文档
- [简介](https://cdhigh.github.io/Chinese/1.intro.html)
+- [配置项描述](https://cdhigh.github.io/Chinese/2.config.html)
+- [部署方法](https://cdhigh.github.io/Chinese/3.deployment.html)
+ - [Google cloud (PaaS)](https://cdhigh.github.io/Chinese/3.deployment.html#gae)
+ - [Oracle cloud (VPS)](https://cdhigh.github.io/Chinese/3.deployment.html#oracle-cloud)
+ - [Pythonanywhere (PaaS)](https://cdhigh.github.io/Chinese/3.deployment.html#python-anywhere)
-- [部署方法](https://cdhigh.github.io/Chinese/2.deployment.html)
- - [Google cloud](https://cdhigh.github.io/Chinese/2.deployment.html#gae)
- - [Pythonanywhere](https://cdhigh.github.io/Chinese/2.deployment.html#pythonanywhere)
- - [Ubuntu](https://cdhigh.github.io/Chinese/2.deployment.html#ubuntu)
-
-- [FAQ](https://cdhigh.github.io/Chinese/3.faq.html)
+- [FAQ](https://cdhigh.github.io/Chinese/4.faq.html)
diff --git a/docs/English/1.intro.md b/docs/English/1.intro.md
index 3314e1d3..e04cdd7f 100644
--- a/docs/English/1.intro.md
+++ b/docs/English/1.intro.md
@@ -3,10 +3,7 @@ sort: 1
---
# Intro
-KindleEar is an open-source and free web application that can be deployed on most hosting platforms that support Python, including but not limited to Google Cloud, Heroku, PythonAnywhere, VPS, Ubuntu, Raspberry Pi, etc. Its main function is to automatically collect web articles via RSS at regular intervals and compile them into illustrated eBooks, which are then pushed to your Kindle or other e-reader devices.
+KindleEar is an open-source and free web application that can be deployed on most hosting platforms that support Python, including but not limited to Google Cloud, Heroku, VPS, Ubuntu, Raspberry Pi, etc. Its main function is to automatically collect web articles via RSS at regular intervals and compile them into illustrated eBooks, which are then pushed to your Kindle or other e-reader devices.
KindleEar has modified and extracted the epub/mobi generation module of the famous e-book management software Calibre. In addition to being able to push by simply inputting RSS feed URLs, it also directly supports Calibre's Recipe format (Python scripts that capture information from various websites). KindleEar has built-in over a thousand Recipes covering multiple languages. Besides, users can also write their own Recipes and upload them to KindleEar via the management page.
-
-
-
diff --git a/docs/English/2.deployment.md b/docs/English/2.config.md
similarity index 78%
rename from docs/English/2.deployment.md
rename to docs/English/2.config.md
index 71f1ed80..41be75ba 100644
--- a/docs/English/2.deployment.md
+++ b/docs/English/2.config.md
@@ -1,23 +1,22 @@
---
sort: 2
---
-# Deployment Steps
+# Configuration
-
-## config.py
+
+## Base config
Regardless of the platform you deploy to, the first step is to correctly configure config.py.
This section describes several simple configuration items, with more detailed descriptions of other configuration items in the subsequent sections.
| Configuration Item | Meaning |
| ------------------ | ------------------------------------------------------ |
| APP_ID | Application identifier; for GAE platform, it's the app ID, while for other platforms, it's used to identify database and other resources |
-| SRC_EMAIL | Sender email address for sending emails |
| APP_DOMAIN | Domain name of the deployed application |
| TEMP_DIR | Temporary directory for creating eBooks; if empty, temporary files are stored in memory |
| DOWNLOAD_THREAD_NUM| Number of threads for downloading web pages; the target platform needs to support multithreading, with a maximum value of 5 |
| ALLOW_SIGNUP | Whether to allow user registration; False means account creation is done by administrators |
+| SECRET_KEY | Encryption key for browser session, recommended to change, any string is acceptable |
| ADMIN_NAME | Administrator's username |
-| TIMEZONE | Default timezone for new accounts; users can modify their timezone on the web page after account creation |
| POCKET_CONSUMER_KEY| Used for Pocket's read-later service; you can use your own key or use this one directly |
| HIDE_MAIL_TO_LOCAL| Whether to allow saving generated emails locally for debugging or testing purposes |
@@ -28,6 +27,7 @@ This section describes several simple configuration items, with more detailed de
The database is used to store application configuration data and subscription data.
Thanks to the SQL database ORM library [peewee](https://pypi.org/project/peewee/) and the NoSQL database ODM library [weedata](https://pypi.org/project/weedata/) created by the author for KindleEar, KindleEar supports many types of databases, including: datastore, sqlite, mysql, postgresql, cockroachdb, mongodb, redis, pickle.
It basically covers the mainstream databases on the market and is more suitable for cross-platform deployment. You can use whatever database the platform supports.
+If the target platform supports both SQL and NoSQL, it is advisable to use NoSQL. Its major advantage is that in case of future upgrades requiring modifications to the database structure, NoSQL will not affect the existing data, whereas SQL would delete the original data.
The amount of data in this application is very small, to be precise, very, very small, usually just a few dozen lines of data. Choosing any database will not have any impact on resource consumption and performance. Even using a simple text file as a database may be faster than other formal databases.
@@ -36,7 +36,7 @@ The amount of data in this application is very small, to be precise, very, very
### datastore
Datastore is Google's NoSQL database, and we will be using the Datastore mode of Firebase. If you want to deploy to Google Cloud, basically, you can only choose Datastore because it has free quotas.
To use Datastore, the parameter configuration is as follows:
-```
+```python
DATABASE_URL = 'datastore'
```
@@ -45,7 +45,7 @@ DATABASE_URL = 'datastore'
### SQLite
SQLite is a single-file database. It is suitable for platforms with local file system read and write permissions, especially resource-constrained systems such as Raspberry Pi and various derivatives.
To use SQLite, the parameter configuration is as follows:
-```
+```python
#template:
DATABASE_URL = 'sqlite:////path/to/database.db'
#examples:
@@ -58,7 +58,7 @@ DATABASE_URL = 'sqlite:////home/username/dbfilename.db'
### MySQL/PostgreSQL/CockroachDB
These are typical enterprise-level SQL databases. It's like using a cannon to kill a mosquito, but if the platform supports it, there's no harm in using them directly.
Parameter configuration is as follows:
-```
+```python
#template:
DATABASE_URL = 'mysql://username:password@hostname:port/database_name'
DATABASE_URL = 'postgresql://username:password@hostname:port/database_name'
@@ -83,7 +83,7 @@ database_url = f"mysql://{db_username}:{db_password}@{db_host}:{db_port}/{db_nam
### MongoDB
The most widely used typical NoSQL database.
Parameter configuration is as follows:
-```
+```python
#template:
DATABASE_URL = 'mongodb://username:password@hostname:port/'
#examples:
@@ -94,11 +94,11 @@ DATABASE_URL = 'mongodb://user:pass123@example.com:27017/'
### Redis
-A memory database that can persist to disk.
+A memory NoSQL database that can persist to disk.
If the target system already has Redis installed and used for task queues, using Redis directly can save the resource consumption of installing other databases.
However, before using it, relevant Redis persistence configurations should be done to avoid data loss.
Parameter configuration is as follows (the db_number can be omitted, but if it's 0, it's recommended to omit it):
-```
+```python
DATABASE_URL = 'redis://[:password]@hostname:port/db_number'
DATABASE_URL = 'redis://127.0.0.1:6379/0'
DATABASE_URL = 'redis://:password123@example.com:6379/1'
@@ -107,10 +107,10 @@ DATABASE_URL = 'redis://:password123@example.com:6379/1'
### Pickle
-A very simple single-file "database" created by the author using Python's pickle data persistence standard library.
+A very simple single-file NoSQL "database" created by the author using Python's pickle data persistence standard library.
It can be used for resource-constrained systems or for testing purposes.
Parameter configuration is as follows:
-```
+```python
#template:
DATABASE_URL = 'pickle:////path/to/database.db'
#examples:
@@ -130,7 +130,7 @@ Scheduler tasks are used for periodically checking whether there is a need for p
### gae
If you want to deploy to Google Cloud, you can only choose GAE.
-```
+```python
TASK_QUEUE_SERVICE = "gae"
TASK_QUEUE_BROKER_URL = ""
```
@@ -141,7 +141,7 @@ TASK_QUEUE_BROKER_URL = ""
Comparatively lightweight, with the simplest configuration, it can work without relying on Redis or other databases by directly using memory to store task states.
However, there is a risk of losing tasks. If power is lost during the execution of a task, the original task will not rerun after power is restored; it will only wait for the next scheduled time.
If database persistence is required, it supports SQLite/MySQL/PostgreSQL/MongoDB/Redis, and you can configure it with the same value as DATABASE_URL.
-```
+```python
TASK_QUEUE_SERVICE = "apscheduler"
TASK_QUEUE_BROKER_URL = "" # use memory store
@@ -156,7 +156,7 @@ TASK_QUEUE_BROKER_URL = "sqlite:////home/username/dbfilename.db"
### celery
The most famous task queue, supports various backends such as Redis, MongoDB, SQL, shared directories, etc.
If database persistence for task states is required, it can be configured with the same value as DATABASE_URL.
-```
+```python
TASK_QUEUE_SERVICE = "celery"
TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
@@ -171,7 +171,7 @@ TASK_QUEUE_BROKER_URL = "file:////?/C:/Users/name/results/" # keep the prefix 'f
### rq
Slightly lighter-weight than Celery, it depends on Redis and requires an additional installation of the Redis service.
-```
+```python
TASK_QUEUE_SERVICE = "celery"
TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
```
@@ -182,9 +182,21 @@ TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
## Email Sending Service Selection
To make it more convenient to use and avoid some limitations of free quotas, the email sending service can be configured on the web page after deployment is complete.
-* **GAE**: Recommended for deployment to Google Cloud, with sufficient quotas and generous email size limit, with a maximum of 31.5MB per email.
-* **SendGrid**: Recommended for deployment to other platforms, requires additional [registration](https://sendgrid.com/) and application for an API key, with a maximum of 30MB per email.
-* **SMTP**: This option is flexible. Most email service platforms support SMTP, and it's also convenient to deploy your own SMTP service on platforms like Ubuntu.
+* **GAE**:
+Recommended for deployment to Google Cloud, with sufficient quotas and generous email size limit, with a maximum of 31.5MB per email.
+
+* **SendGrid**:
+Recommended for deployment to other platforms, requires additional [registration](https://sendgrid.com/) and application for an API key, with a maximum of 30MB per email.
+Note: I have been unable to register with SendGrid successfully. Regardless of the methods used or the number of email addresses tried, I cannot login to SendGrid. Therefore, I have not personally tested this functionality. If any friends have successfully tested it, please inform me.
+
+* **mailjet**:
+You can also use [Mailjet](https://www.mailjet.com/). Just sign up for an account and get your ApiKey and SecretKey. The largest email you can send is 15MB. Don't forget to check that your sender address is on the [Sender addresses](https://app.mailjet.com/account/sender) list before you send any mail.
+During testing, another issue was discovered: if your sender email address is not the one you registered with Mailjet, Mailjet will not report an error; instead, the recipient will never receive the email. Therefore, if sending emails fails with Mailjet, please verify that the sender address is correct.
+
+* **SMTP**:
+This option is flexible, as most email service platforms support SMTP. However, many platforms have various restrictions on SMTP. Before use, please carefully read the relevant instructions, especially considering that the SMTP password for most platforms differs from the regular account password.
+
+In addition to using existing services available in the market, platforms like Ubuntu also offer convenient ways to deploy your own SMTP service.
@@ -192,14 +204,14 @@ To make it more convenient to use and avoid some limitations of free quotas, the
## Email Receiving Service
The GAE platform has a unique service. After the application is deployed, besides sending emails, it can also receive emails, which can be quite useful sometimes. For specific usage, refer to the [FAQ](3.faq.html#appspotmail).
If you want to enable this service, configure as follows (Note: only supported on GAE platform, setting on other platforms won't work):
-```
+```python
INBOUND_EMAIL_SERVICE = 'gae'
```
-
-## Web Server
+
+## WSGI container
KindleEar uses the Flask framework to implement web interface management, with the entry point being `main.app` (the app instance object inside the main.py file).
You can start this app using any web server software that supports the WSGI standard interface.
For low requirements, you can directly start with the Flask debug server.
@@ -212,7 +224,7 @@ The same applies to other target platforms; choose whichever you prefer or are f
## requirements.txt
KindleEar uses requirements.txt to manage dependencies on various libraries.
It allows for easy environment configuration with just one command on various platforms.
-```
+```bash
pip install -r requirements.txt
```
@@ -223,19 +235,3 @@ Alternatively, you can choose not to use this script and remove all comments in
-
-
-## Google Cloud
-1. Download the latest version of KindleEar from the GitHub page. In the bottom right corner of the page, there's a button labeled "Download ZIP". Clicking it will download a ZIP document containing all the source code. Then, unzip it to a directory of your choice, such as D:\KindleEar.
-2. Install the standard environment Google Cloud SDK/Google Cloud CLI, and then execute:
-```
-gcloud init
-gcloud auth login
-gcloud config set project your_app_id
-gcloud app deploy --version=1 path\to\app.yaml
-gcloud app deploy --version=1 path\to\kindleear
-gcloud app deploy --version=1 path\to\cron.yaml
-gcloud app deploy --version=1 path\to\queue.yaml
-gcloud datastore indexes create path\to\index.yaml
-```
-
diff --git a/docs/English/3.deployment.md b/docs/English/3.deployment.md
new file mode 100644
index 00000000..7dc3d942
--- /dev/null
+++ b/docs/English/3.deployment.md
@@ -0,0 +1,164 @@
+---
+sort: 3
+---
+# Deployment
+KindleEar supports deployment on multiple platforms. I will only list some platforms that I have tested here. You are welcome to add deployment guides for other platforms.
+
+
+## Google Cloud (PaaS)
+1. config.py Key Parameter Example
+```python
+SERVER_LOCATION = "us-central1"
+DATABASE_URL = "datastore"
+INBOUND_EMAIL_SERVICE = "gae"
+TASK_QUEUE_SERVICE = "gae"
+TASK_QUEUE_BROKER_URL = ""
+```
+
+2. Download the latest version of KindleEar from the GitHub page. In the bottom right corner of the page, there's a button labeled "Download ZIP". Clicking it will download a ZIP document containing all the source code. Then, unzip it to a directory of your choice, such as D:\KindleEar.
+
+3. Install [gloud CLI](https://cloud.google.com/sdk/docs/install), and then execute:
+```bash
+gcloud components install app-engine-python app-engine-python-extras # Run as Administrator
+gcloud init
+gcloud auth login
+gcloud auth application-default set-quota-project your_app_id
+gcloud config set project your_app_id
+gcloud beta app deploy --version=1 app.yaml
+gcloud beta app deploy --version=1 cron.yaml
+gcloud beta app deploy --version=1 queue.yaml
+```
+
+4. If encountering errors like "Timed out fetching pod", you have the option to delete this app id, recreate a new one and select a different region during deployment.
+
+5. After successful deployment, go to the [GAE console](https://console.cloud.google.com/appengine/settings/emailsenders) and add your sender address to "Mail API Authorized Senders" to prevent "Unauthorized sender" errors during delivery.
+
+
+
+
+
+## Oracle cloud (VPS)
+1. config.py Key Parameter Example
+```python
+DATABASE_URL = "sqlite:////home/ubuntu/site/kindleear/database.db"
+INBOUND_EMAIL_SERVICE = ""
+TASK_QUEUE_SERVICE = "apscheduler"
+TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/"
+TEMP_DIR = "/tmp"
+DOWNLOAD_THREAD_NUM = 1
+```
+
+2. Create a compute instance, with the recommended configuration being "Always Free".
+Choose an image that you are familiar with, I selected Ubuntu minimal.
+Remember to download and save the private ssh key. Once created, click on the "Subnet" link on "Instance Details" page then modify or create inbound rules in the "Security Lists" by removing TCP ports and ICMP types and codes.
+Test ping the corresponding IP, if successful, it indicates that the instance configuration is complete.
+
+3. Connect remotely to the instance using your preferred SSH tool.
+3.1 If using puTTY, first convert the ssh private key to ppk format using puttyGen.
+Open puTTY, the Host format as username@IP, port 22. You can find the username in the "Instance Details" page. Import the private key file under Connection|SSH|Auth|Credentials.
+3.2 If using Xshell, choose "Public Key" for authentication method and import the previously saved private key file.
+
+4. Upon login, it is recommended to first change the root password.
+```bash
+sudo -i
+passwd
+```
+
+
+5. Talk is cheap, show me commands.
+```bash
+sudo apt update
+sudo apt upgrade
+sudo apt install nginx
+sudo apt install git python3.10 python3-pip
+sudo pip3 install virtualenv
+sudo apt install redis-server
+sudo systemctl start nginx
+sudo systemctl start redis-server
+sudo systemctl enable nginx.service
+sudo systemctl enable redis-server.service
+
+curl localhost #test if nginx works well
+
+sudo apt install vim-common
+mkdir ~/site
+mkdir ~/log
+cd ~/site
+
+#fetch code from github, or you can upload code files by xftp/scp
+git clone https://github.com/cdhigh/kindleear.git
+cd kindleear
+virtualenv --python=python3 venv #create virtual environ
+vim ./config.py #start to modify some config items
+python3 ./tools/deploy_helper.py #update requirements.txt
+
+source ./venv/bin/activate #activate virtual environ
+pip install -r requirements.txt #install dependencies
+python3 ./main.py db create #create database tables
+
+#open port 80
+sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 80 -j ACCEPT
+sudo netfilter-persistent save
+
+#modify nginx configuration
+#1. change the first line to 'user loginname;', for example 'user ubuntu;'
+#2. add a line 'client_max_body_size 16M;' to http section
+sudo vim /etc/nginx/nginx.conf
+vim ./tools/nginx/default #change server_name if you want
+sudo ln -f ./tools/nginx/default /etc/nginx/sites-enabled/default
+sudo nginx -t #test if nginx config file is correct
+
+#set gunicorn auto start
+sudo cp ./tools/nginx/gunicorn.service /usr/lib/systemd/system
+sudo systemctl daemon-reload
+sudo systemctl start gunicorn.service
+sudo systemctl status gunicorn.service
+sudo systemctl enable gunicorn.service
+
+sudo systemctl restart nginx.service
+sudo systemctl status nginx.service
+```
+
+6. Now you can digit "http://ip" in your browser to confirm if the deployment was successful.
+If desired, you can also continue to configure nginx to use SSL.
+If you already have a domain name, you can also bind it.
+
+7. To check for errors, use the following commands to query the backend logs:
+```bash
+cat /var/log/nginx/error.log
+cat /home/ubuntu/log/gunicorn.error.log
+cat /home/ubuntu/log/gunicorn.access.log
+```
+
+8. Epilogue: If you choose Oracle Cloud, it is recommended to enable their "OCI Email Delivery" service and utilize SMTP for sending emails. This service supports single email up to 60MB, which I have yet to find supported by any other service provider.
+
+
+
+
+
+
+## PythonAnywhere (PaaS)
+1. config.py Key Parameter Example
+```python
+DATABASE_URL = "mysql://name:pass@name.mysql.pythonanywhere-services.com/name$default"
+INBOUND_EMAIL_SERVICE = ""
+TASK_QUEUE_SERVICE = ""
+TASK_QUEUE_BROKER_URL = ""
+```
+
+2. Log in to [PythonAnywhere](https://www.pythonanywhere.com), go to the "Web" tab, click "Add a new web app" on the left, and create a Flask application.
+
+3. Go to the "Databases" tab, initialize mysql and create a database.
+
+4. Refer to the [UploadingAndDownloadingFiles](https://help.pythonanywhere.com/pages/UploadingAndDownloadingFiles) documentation to upload the code using the git or zip method.
+
+5. Open a Bash console in the "Files" tab and execute the bash command `pip install -r requirements.txt`.
+6. Create a scheduled task. PythonAnywhere does not support setting scheduled tasks freely in the code, and free users can only set one scheduled task, which is a big limitation. However, if you want to try it, you can go to the "Tasks" tab and create a Task according to the time you want to push the subscription. The bash command is:
+`python /home/yourname/yourdirectory/main.py deliver now`
+
+If deployed on PythonAnywhere, the delivery time setting on the web page is invalid, and the delivery time is the time of this Task.
+
+7. If you are a free user, you need to log in to PythonAnywhere at least once every three months and click "Run until 3 months from today". Otherwise, your application will be suspended.
+
+**Note:** After testing, it is not suitable for our application deployment unless paid, because it has many restrictions. The most fatal restriction is that it implements a whitelist for websites that free users can access. Websites not in its [list](https://www.pythonanywhere.com/whitelist/) cannot be accessed.
+
diff --git a/docs/English/3.faq.md b/docs/English/4.faq.md
similarity index 95%
rename from docs/English/3.faq.md
rename to docs/English/4.faq.md
index afdcfeaf..054e57df 100644
--- a/docs/English/3.faq.md
+++ b/docs/English/4.faq.md
@@ -1,5 +1,5 @@
---
-sort: 3
+sort: 4
---
# FAQ
@@ -9,15 +9,18 @@ Full-text RSS is the term I use for this type of RSS, and I don't know what the
How to confirm whether the RSS you want to subscribe to is full-text RSS? It's simple. Use a browser to open the corresponding link of the RSS and see if all the article content is already there. If it is, then it's full-text RSS. If only article summaries are provided, then it's not.
+
## Can full-text RSS be treated as summary RSS, and vice versa?
Of course, full-text RSS can be treated as summary RSS, which ignores the article content provided in the RSS link and directly fetches it from the original link, but it takes a little longer, resulting in a decrease in the number of supported RSS feeds. If it's summary RSS, it cannot be treated as full-text RSS, otherwise it will result in incomplete article content.
+
## How to customize the delivery time for a Recipe?
In addition to setting a unified delivery day and time on the settings page, each Recipe can customize its own unique delivery day and time. Once set, the unified time setting will be ignored for this Recipe. The method is to click on the circular button next to a Recipe in the "Subscribed" section of "My Feeds," then use the "Customize delivery time" button that pops up to set it. You can set it to push only on certain days or multiple times a day.
However, custom push time is only applicable to built-in and uploaded Recipes. Custom RSS feeds use only the unified time set. If you want to set the push time for a custom RSS feed, you can write its title and URL into a Recipe, and then upload it to start setting.
+
## How to customize the cover?
KindleEar has 7 built-in covers, which are randomly selected by default or can be configured to be selected by day of the week. You can upload your own favorite covers to replace these built-in covers. The entry is in the "Cover Image" under "Advanced" section.
If you want to set a cover for a specific Recipe, you need to add a cover_url attribute in its Recipe source code. It can be a local file (if it's a relative directory, it's relative to the KindleEar application directory) or a web image, for example:
@@ -29,8 +32,10 @@ cover_url = 'https://www.google.com/mycover.jpg'
Additionally, if you want to customize the masthead, add a masthead_url attribute, which has the same format as cover_url.
+
## What if I forget my password?
-KindleEar does not store passwords in plain text and cannot retrieve them. If login fails due to password verification, a "Forgot Password?" link is provided. Click on this link to reset your password using the email address registered when creating your account. The reset password email address for the administrator is SRC_EMAIL in config.py.
+KindleEar does not store passwords in plain text and cannot retrieve them. If login fails due to password verification, a "Forgot Password?" link is provided. Click on this link to reset your password using the email address registered when creating your account.
+
@@ -43,7 +48,8 @@ If your application is deployed on the Google Cloud Platform (GAE), KindleEar al
5. By default, it's sent to the email registered by the administrator. If you want to send it to another user's email, use the format: username__xxx@appid.appspotmail.com. (Note the double underscore)
6. If you send the download link of an e-book to book@appid.appspotmail.com or username__book@appid.appspotmail.com, KindleEar will directly download the corresponding e-book archive and forward it to the registered email address. (Note: GAE has restrictions on file extensions that can be emailed and cannot send file extensions that may have security risks such as exe, zip files can be sent, but zip files cannot contain files with potential security risks.) GAE's list of email-able file extensions: [Mail Python API Overview](https://cloud.google.com/appengine/docs/python/mail/#Python_Sending_mail_with_attachments) (book/file/download email addresses reserved for downloading e-books)
7. Sending to trigger@appid.appspotmail.com or username__trigger@appid.appspotmail.com triggers a manual delivery. If the subject is empty or all, it's exactly the same as the "Deliver Now" button on the webpage. If you need to push specific books, write the book name in the subject, separated by commas.
-8. Emails sent to debug@appid.appspotmail.com directly extract the links from the email and send HTML files directly to the administrator's email (SRC_EMAIL) instead of the Kindle email.
+8. Emails sent to debug@appid.appspotmail.com directly extract the links from the email and send HTML files directly to the administrator's email instead of the Kindle email.
+
## What if some websites require login to read articles?
@@ -57,13 +63,11 @@ Then, after subscribing, you can select "Website Login Information" from the cor
3. The password you enter is encrypted and saved, with a unique 8-character random string key for each account, which has a certain level of security. I try my best to take care of your password security. You can delete the saved password information at any time, and the password is also deleted immediately after unsubscribing from the book. However, because the key is also saved in database, the security cannot be guaranteed to be very high. Please understand and be willing to bear the risks involved.
+
## What's the difference between "Subscribe" and "Subscribe (Deliver Separately)"?
"Subscribe" is for combined delivery, which combines all Recipes and custom RSS feeds subscribed to with this option into one file for delivery. "Subscribe (Deliver Separately)" creates a separate file for this Recipe for delivery, which is more suitable for Recipes that generate large file or have special delivery times.
-## Why does the delivery log status show wrong SRC_EMAIL?
-Go to the Settings page of [GAE backend](https://console.cloud.google.com/appengine) and check if your sending email address is in the list of authorized emails. If not, add it.
-
## I have more questions, where can I ask?
If you have more questions, you can submit an issue at [https://github.com/cdhigh/KindleEar/issues](https://github.com/cdhigh/KindleEar/issues) and wait for a reply. Before submitting a question, it's recommended to search for previously submitted issues first. Maybe someone has already submitted a similar issue? If no one has submitted a similar issue, when you submit a new one, it's recommended to attach the Logs information of [GAE backend](https://console.cloud.google.com/appengine) or the platform you deployed to for problem location, which can also get you a faster reply.
diff --git a/docs/English/README.md b/docs/English/README.md
index 63dfe096..3a7f1985 100644
--- a/docs/English/README.md
+++ b/docs/English/README.md
@@ -4,7 +4,10 @@ sort: 1
# English docs
- [Intro](https://cdhigh.github.io/English/1.intro.html)
-
-- [Deployment](https://cdhigh.github.io/English/2.deployment.html)
+- [Configuration](https://cdhigh.github.io/English/2.config.html)
+- [Deployment](https://cdhigh.github.io/English/3.deployment.html)
+ - [Google cloud (PaaS)](https://cdhigh.github.io/English/3.deployment.html#gae)
+ - [Oracle cloud (VPS)](https://cdhigh.github.io/English/3.deployment.html#oracle-cloud)
+ - [Pythonanywhere (PaaS)](https://cdhigh.github.io/English/3.deployment.html#python-anywhere)
-- [FAQ](https://cdhigh.github.io/English/3.faq.html)
+- [FAQ](https://cdhigh.github.io/English/4.faq.html)
diff --git a/docs/README.md b/docs/README.md
index 343da2d6..fd29200f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,3 +1,5 @@
# KindleEar docs
-KindleEar is a free webapp that running on Google App Engine, it aggregates feeds and deliver the generated docs to your Kindle periodically.
\ No newline at end of file
+KindleEar is an open-source and free web application that can be deployed on most hosting platforms that support Python, including but not limited to Google Cloud, Heroku, PythonAnywhere, VPS, Ubuntu, Raspberry Pi, etc. Its main function is to automatically collect web articles via RSS at regular intervals and compile them into illustrated eBooks, which are then pushed to your Kindle or other e-reader devices.
+
+KindleEar has modified and extracted the epub/mobi generation module of the famous e-book management software Calibre. In addition to being able to push by simply inputting RSS feed URLs, it also directly supports Calibre's Recipe format (Python scripts that capture information from various websites). KindleEar has built-in over a thousand Recipes covering multiple languages. Besides, users can also write their own Recipes and upload them to KindleEar via the management page.
\ No newline at end of file
diff --git a/changelog.md b/docs/changelog.md
similarity index 100%
rename from changelog.md
rename to docs/changelog.md
diff --git a/changelog_en.md b/docs/changelog_en.md
similarity index 100%
rename from changelog_en.md
rename to docs/changelog_en.md
diff --git a/docs/scrshot.gif b/docs/scrshot.gif
new file mode 100644
index 00000000..497cdcb1
Binary files /dev/null and b/docs/scrshot.gif differ
diff --git a/index.yaml b/index.yaml
deleted file mode 100644
index 5c1941e0..00000000
--- a/index.yaml
+++ /dev/null
@@ -1,32 +0,0 @@
-indexes:
-
-# AUTOGENERATED
-
-# This index.yaml is automatically updated whenever the dev_appserver
-# detects that a new type of query is run. If you want to manage the
-# index.yaml file manually, remove the above marker line (the line
-# saying "# AUTOGENERATED"). If you want to manage some indexes
-# manually, move them above the marker line. The index.yaml file is
-# automatically uploaded to the admin console when you next deploy
-# your application using appcfg.py.
-
-- kind: SharedRss
- properties:
- - name: last_subscribed_time
- direction: desc
-
-- kind: DeliverLog
- properties:
- - name: datetime
- direction: desc
-
-- kind: BookedRecipe
- properties:
- - name: time
- direction: desc
-
-- kind: Recipe
- properties:
- - name: time
- direction: desc
-
\ No newline at end of file
diff --git a/main.py b/main.py
index 9f0f3ec7..e4c28a13 100644
--- a/main.py
+++ b/main.py
@@ -6,8 +6,10 @@
import os, sys, builtins, logging
appDir = os.path.dirname(os.path.abspath(__file__))
-log = logging.getLogger()
-log.setLevel(logging.DEBUG) #logging.DEBUG
+log = logging.getLogger('gunicorn.error')
+if log.level == logging.NOTSET:
+ log.setLevel(logging.WARNING) #logging.DEBUG
+
builtins.__dict__['default_log'] = log
builtins.__dict__['appDir'] = appDir
sys.path.insert(0, os.path.join(appDir, 'application', 'lib'))
@@ -26,8 +28,9 @@ def set_env():
os.environ['DATABASE_URL'] = DATABASE_URL
os.environ['TASK_QUEUE_SERVICE'] = TASK_QUEUE_SERVICE
os.environ['TASK_QUEUE_BROKER_URL'] = TASK_QUEUE_BROKER_URL
- os.environ['APP_DOMAIN'] = 'http://127.0.0.1:5000/' #APP_DOMAIN
- os.environ['SRC_EMAIL'] = SRC_EMAIL
+ os.environ['APP_ID'] = APP_ID
+ os.environ['APP_DOMAIN'] = APP_DOMAIN
+ os.environ['SERVER_LOCATION'] = SERVER_LOCATION
os.environ['ADMIN_NAME'] = ADMIN_NAME
os.environ['HIDE_MAIL_TO_LOCAL'] = '1' if HIDE_MAIL_TO_LOCAL else ''
@@ -37,11 +40,8 @@ def set_env():
app = init_app(__name__, debug=False)
celery_app = app.extensions.get("celery", None)
-from application.back_end.db_models import create_database_tables
-create_database_tables()
-
def main():
- if len(sys.argv) <= 1:
+ if len(sys.argv) == 2 and sys.argv[1] == 'debug':
#os.environ['DATASTORE_DATASET'] = app.config['APP_ID']
#os.environ['DATASTORE_EMULATOR_HOST'] = 'localhost:8081'
#os.environ['DATASTORE_EMULATOR_HOST_PATH'] = 'localhost:8081/datastore'
@@ -52,25 +52,34 @@ def main():
app.run(host='0.0.0.0', debug=False)
return 0
elif len(sys.argv) >= 3:
- from application.view.deliver import MultiUserDelivery
- from application.work.worker import WorkerAllNow
- act = sys.argv[1].lower()
- param = sys.argv[2].lower()
+ act = sys.argv[1]
+ param = sys.argv[2]
if (act == 'deliver') and (param == 'check'):
- result = MultiUserDelivery()
- print(result)
+ from application.view.deliver import MultiUserDelivery
+ print(MultiUserDelivery())
return 0
elif (act == 'deliver') and (param == 'now'):
- result = WorkerAllNow()
- print(result)
+ from application.work.worker import WorkerAllNow
+ print(WorkerAllNow())
+ return 0
+ elif (act == 'db') and (param == 'create'):
+ from application.back_end.db_models import create_database_tables
+ print(create_database_tables())
return 0
+ elif (act == 'log') and (param == 'purge'):
+ from application.view.logs import RemoveLogs
+ print(RemoveLogs())
+ return 0
+
print(f'\nKindleEar Application {appVer}')
- print('\nUsage: main.py [debug | deliver check | deliver now]')
+ print('\nUsage: main.py commands')
print('\ncommands:')
print(' debug \t Run the application in debug mode')
+ print(' db create \t Create database tables')
print(' deliver check\t Start delivery if time set is matched')
print(' deliver now \t Force start delivery task')
+ print(' log purge \t remove logs older than one month')
print('\n')
if __name__ == "__main__":
diff --git a/messages.pot b/messages.pot
index b58abd5f..157c7f34 100644
--- a/messages.pot
+++ b/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2024-02-27 22:09-0300\n"
+"POT-Creation-Date: 2024-03-10 21:31-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -75,15 +75,15 @@ msgstr ""
#: application/templates/admin.html:43
#: application/templates/adv_whitelist.html:29 application/templates/my.html:32
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add"
msgstr ""
#: application/templates/admin.html:54 application/templates/home.html:15
-#: application/templates/login.html:21 application/templates/logs.html:61
+#: application/templates/login.html:21 application/templates/logs.html:65
#: application/templates/reset_password.html:19
#: application/templates/reset_password.html:20
-#: application/templates/setting.html:211 application/templates/signup.html:21
+#: application/templates/setting.html:218 application/templates/signup.html:21
#: application/templates/user_account.html:15
msgid "Username"
msgstr ""
@@ -189,7 +189,7 @@ msgstr ""
#: application/templates/adv_archive.html:55 application/templates/base.html:47
#: application/templates/home.html:16 application/templates/login.html:25
-#: application/templates/setting.html:215 application/templates/signup.html:25
+#: application/templates/setting.html:222 application/templates/signup.html:25
#: application/templates/user_account.html:19
msgid "Password"
msgstr ""
@@ -198,8 +198,8 @@ msgstr ""
msgid "Verify"
msgstr ""
-#: application/templates/adv_archive.html:93
-#: application/templates/setting.html:230
+#: application/templates/adv_archive.html:89
+#: application/templates/setting.html:243
msgid "Save settings"
msgstr ""
@@ -477,8 +477,8 @@ msgstr ""
msgid "Verified"
msgstr ""
-#: application/templates/base.html:59 application/view/login.py:75
-#: application/view/share.py:145
+#: application/templates/base.html:59 application/view/login.py:72
+#: application/view/share.py:154
msgid "The username does not exist or password is wrong."
msgstr ""
@@ -574,14 +574,14 @@ msgstr ""
msgid "The account have been deleted."
msgstr ""
-#: application/templates/base.html:83 application/view/admin.py:199
-#: application/view/share.py:135
+#: application/templates/base.html:83 application/view/admin.py:201
+#: application/view/share.py:144
msgid "The username or password is empty."
msgstr ""
#: application/templates/base.html:84 application/view/admin.py:86
-#: application/view/admin.py:163 application/view/admin.py:203
-#: application/view/login.py:211 application/view/login.py:271
+#: application/view/admin.py:165 application/view/admin.py:205
+#: application/view/login.py:213 application/view/login.py:273
msgid "The two new passwords are dismatch."
msgstr ""
@@ -593,7 +593,7 @@ msgstr ""
msgid "Account added successfully."
msgstr ""
-#: application/templates/base.html:87 application/view/login.py:121
+#: application/templates/base.html:87 application/view/login.py:123
msgid "login required"
msgstr ""
@@ -705,8 +705,8 @@ msgstr ""
msgid "Search"
msgstr ""
-#: application/templates/login.html:34 application/view/login.py:188
-#: application/view/login.py:195
+#: application/templates/login.html:34 application/view/login.py:190
+#: application/view/login.py:197
msgid ""
"The website does not allow registration. You can ask the owner for an "
"account."
@@ -716,11 +716,11 @@ msgstr ""
msgid "Only display last 10 logs"
msgstr ""
-#: application/templates/logs.html:21 application/templates/logs.html:62
+#: application/templates/logs.html:21 application/templates/logs.html:66
msgid "Time"
msgstr ""
-#: application/templates/logs.html:22 application/templates/logs.html:63
+#: application/templates/logs.html:22 application/templates/logs.html:67
#: application/templates/my.html:17 application/templates/setting.html:98
#: application/templates/setting.html:99 application/templates/setting.html:100
#: application/templates/setting.html:101
@@ -728,23 +728,23 @@ msgstr ""
msgid "Title"
msgstr ""
-#: application/templates/logs.html:23 application/templates/logs.html:64
+#: application/templates/logs.html:23 application/templates/logs.html:68
msgid "Size"
msgstr ""
-#: application/templates/logs.html:24 application/templates/logs.html:65
+#: application/templates/logs.html:24 application/templates/logs.html:69
msgid "To"
msgstr ""
-#: application/templates/logs.html:25 application/templates/logs.html:66
+#: application/templates/logs.html:25 application/templates/logs.html:70
msgid "Status"
msgstr ""
-#: application/templates/logs.html:45
+#: application/templates/logs.html:49
msgid "There is no log"
msgstr ""
-#: application/templates/logs.html:49
+#: application/templates/logs.html:53
msgid "Logs of other users"
msgstr ""
@@ -917,33 +917,43 @@ msgstr ""
msgid "Service"
msgstr ""
-#: application/templates/setting.html:199
+#: application/templates/setting.html:202
msgid "ApiKey"
msgstr ""
-#: application/templates/setting.html:203
+#: application/templates/setting.html:206
+msgid "SecretKey"
+msgstr ""
+
+#: application/templates/setting.html:210
msgid "Host"
msgstr ""
-#: application/templates/setting.html:207
+#: application/templates/setting.html:214
msgid "Port"
msgstr ""
-#: application/templates/setting.html:219
+#: application/templates/setting.html:226
msgid "Save path"
msgstr ""
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
#, python-format
msgid ""
"Important: Please activate your kindle firstly, then goto %(personal)s "
"Page and add %(sender)s to 'Approved Personal Document E-mail List'."
msgstr ""
-#: application/templates/setting.html:226
+#: application/templates/setting.html:234
msgid "Personal Document Settings"
msgstr ""
+#: application/templates/setting.html:239
+msgid ""
+"You have not yet set up your email address. Please go to the 'Admin' page"
+" to add your email address firstly."
+msgstr ""
+
#: application/templates/signup.html:38
msgid "Invitation code"
msgstr ""
@@ -957,70 +967,70 @@ msgid "Settings Saved!"
msgstr ""
#: application/view/admin.py:61 application/view/admin.py:69
-#: application/view/admin.py:94
+#: application/view/admin.py:96
msgid "Add account"
msgstr ""
-#: application/view/admin.py:68 application/view/admin.py:107
-#: application/view/admin.py:135
+#: application/view/admin.py:68 application/view/admin.py:109
+#: application/view/admin.py:137
msgid "You do not have sufficient privileges."
msgstr ""
-#: application/view/admin.py:82 application/view/admin.py:150
-#: application/view/login.py:207 application/view/login.py:236
+#: application/view/admin.py:82 application/view/admin.py:152
+#: application/view/login.py:209 application/view/login.py:238
#: application/view/setting.py:61 application/view/setting.py:63
-#: application/view/setting.py:65 application/view/share.py:35
+#: application/view/setting.py:65 application/view/share.py:36
msgid "Some parameters are missing or wrong."
msgstr ""
-#: application/view/admin.py:84 application/view/login.py:43
-#: application/view/login.py:213
+#: application/view/admin.py:84 application/view/login.py:40
+#: application/view/login.py:215
msgid "The username includes unsafe chars."
msgstr ""
-#: application/view/admin.py:88 application/view/login.py:215
+#: application/view/admin.py:88 application/view/login.py:217
msgid "Already exist the username."
msgstr ""
-#: application/view/admin.py:91 application/view/admin.py:169
-#: application/view/admin.py:196 application/view/login.py:262
+#: application/view/admin.py:93 application/view/admin.py:171
+#: application/view/admin.py:198 application/view/login.py:264
msgid "The password includes non-ascii chars."
msgstr ""
-#: application/view/admin.py:111 application/view/admin.py:132
-#: application/view/admin.py:161
+#: application/view/admin.py:113 application/view/admin.py:134
+#: application/view/admin.py:163
msgid "The username '{}' does not exist."
msgstr ""
-#: application/view/admin.py:127
+#: application/view/admin.py:129
msgid "The password will not be changed if the fields are empties."
msgstr ""
-#: application/view/admin.py:128 application/view/admin.py:186
+#: application/view/admin.py:130 application/view/admin.py:188
msgid "Change account"
msgstr ""
-#: application/view/admin.py:129 application/view/admin.py:187
+#: application/view/admin.py:131 application/view/admin.py:189
msgid "Change"
msgstr ""
-#: application/view/admin.py:184
+#: application/view/admin.py:186
msgid "Change success."
msgstr ""
-#: application/view/admin.py:201
+#: application/view/admin.py:203
msgid "The old password is wrong."
msgstr ""
-#: application/view/admin.py:208
-msgid "Change password success."
+#: application/view/admin.py:218
+msgid "Changes saved successfully."
msgstr ""
#: application/view/adv.py:76 application/view/adv.py:77
#: application/view/adv.py:78 application/view/adv.py:79
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84 application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Append hyperlink '{}' to article"
msgstr ""
@@ -1047,7 +1057,6 @@ msgstr ""
#: application/view/adv.py:80 application/view/adv.py:81
#: application/view/adv.py:82 application/view/adv.py:83
-#: application/view/adv.py:84
msgid "Share on {}"
msgstr ""
@@ -1056,45 +1065,41 @@ msgid "weibo"
msgstr ""
#: application/view/adv.py:81
-msgid "tencent weibo"
-msgstr ""
-
-#: application/view/adv.py:82
msgid "facebook"
msgstr ""
-#: application/view/adv.py:84
+#: application/view/adv.py:83
msgid "tumblr"
msgstr ""
-#: application/view/adv.py:85
+#: application/view/adv.py:84
msgid "Open in browser"
msgstr ""
-#: application/view/adv.py:359
+#: application/view/adv.py:358
msgid "Authorization Error! {}"
msgstr ""
-#: application/view/adv.py:382
+#: application/view/adv.py:381
msgid "Success authorized by Pocket!"
msgstr ""
-#: application/view/adv.py:388
+#: application/view/adv.py:387
msgid ""
"Failed to request authorization of Pocket! See details "
"below: {}"
msgstr ""
-#: application/view/adv.py:398
+#: application/view/adv.py:397
msgid "Request type [{}] unsupported"
msgstr ""
-#: application/view/adv.py:413
+#: application/view/adv.py:412
msgid "The Instapaper service encountered an error. Please try again later."
msgstr ""
-#: application/view/deliver.py:67 application/view/login.py:158
-#: application/view/share.py:39
+#: application/view/deliver.py:67 application/view/login.py:160
+#: application/view/share.py:40
msgid "The username does not exist or the email is empty."
msgstr ""
@@ -1110,9 +1115,9 @@ msgstr ""
msgid "Cannot fetch data from {}, status: {}"
msgstr ""
-#: application/view/library.py:48 application/view/subscribe.py:191
-#: application/view/subscribe.py:302 application/view/subscribe.py:331
-#: application/view/subscribe.py:339
+#: application/view/library.py:48 application/view/subscribe.py:192
+#: application/view/subscribe.py:303 application/view/subscribe.py:332
+#: application/view/subscribe.py:340
msgid "The recipe does not exist."
msgstr ""
@@ -1120,69 +1125,69 @@ msgstr ""
msgid "Please use {}/{} to login at first time."
msgstr ""
-#: application/view/login.py:39
+#: application/view/login.py:36
msgid "Username is empty."
msgstr ""
-#: application/view/login.py:41
+#: application/view/login.py:38
msgid "The len of username reached the limit of 25 chars."
msgstr ""
-#: application/view/login.py:76
+#: application/view/login.py:73
msgid "Forgot password?"
msgstr ""
-#: application/view/login.py:137 application/view/login.py:273
+#: application/view/login.py:139 application/view/login.py:275
msgid "The token is wrong or expired."
msgstr ""
-#: application/view/login.py:140
+#: application/view/login.py:142
msgid "Please input the correct username and email to reset password."
msgstr ""
-#: application/view/login.py:142
+#: application/view/login.py:144
msgid "The email of account '{name}' is {email}."
msgstr ""
-#: application/view/login.py:163
+#: application/view/login.py:165
msgid "Reset password success, Please close this page and login again."
msgstr ""
-#: application/view/login.py:166
+#: application/view/login.py:168
msgid "The email you input is not associated with this account."
msgstr ""
-#: application/view/login.py:177
+#: application/view/login.py:179
msgid "The link to reset your password has been sent to your email."
msgstr ""
-#: application/view/login.py:178
+#: application/view/login.py:180
msgid "Please check your email inbox within 24 hours."
msgstr ""
-#: application/view/login.py:209
+#: application/view/login.py:211
msgid "The invitation code is invalid."
msgstr ""
-#: application/view/login.py:217
+#: application/view/login.py:219
msgid ""
"Failed to create an account. Please contact the administrator for "
"assistance."
msgstr ""
-#: application/view/login.py:227
+#: application/view/login.py:229
msgid "Successfully created account."
msgstr ""
-#: application/view/login.py:238
+#: application/view/login.py:240
msgid "Reset KindleEar password"
msgstr ""
-#: application/view/login.py:239
+#: application/view/login.py:241
msgid "This is an automated email. Please do not reply to it."
msgstr ""
-#: application/view/login.py:240
+#: application/view/login.py:242
msgid "You can click the following link to reset your KindleEar password."
msgstr ""
@@ -1194,142 +1199,142 @@ msgstr ""
msgid "Title is requied!"
msgstr ""
-#: application/view/setting.py:124
+#: application/view/setting.py:123
msgid "Chinese"
msgstr ""
-#: application/view/setting.py:125
+#: application/view/setting.py:124
msgid "English"
msgstr ""
-#: application/view/setting.py:126
+#: application/view/setting.py:125
msgid "French"
msgstr ""
-#: application/view/setting.py:127
+#: application/view/setting.py:126
msgid "Spanish"
msgstr ""
-#: application/view/setting.py:128
+#: application/view/setting.py:127
msgid "Portuguese"
msgstr ""
-#: application/view/setting.py:129
+#: application/view/setting.py:128
msgid "German"
msgstr ""
-#: application/view/setting.py:130
+#: application/view/setting.py:129
msgid "Italian"
msgstr ""
-#: application/view/setting.py:131
+#: application/view/setting.py:130
msgid "Japanese"
msgstr ""
-#: application/view/setting.py:132
+#: application/view/setting.py:131
msgid "Russian"
msgstr ""
-#: application/view/setting.py:133
+#: application/view/setting.py:132
msgid "Turkish"
msgstr ""
-#: application/view/setting.py:134
+#: application/view/setting.py:133
msgid "Korean"
msgstr ""
-#: application/view/setting.py:135
+#: application/view/setting.py:134
msgid "Arabic"
msgstr ""
-#: application/view/setting.py:136
+#: application/view/setting.py:135
msgid "Czech"
msgstr ""
-#: application/view/setting.py:137
+#: application/view/setting.py:136
msgid "Dutch"
msgstr ""
-#: application/view/setting.py:138
+#: application/view/setting.py:137
msgid "Greek"
msgstr ""
-#: application/view/setting.py:139
+#: application/view/setting.py:138
msgid "Hindi"
msgstr ""
-#: application/view/setting.py:140
+#: application/view/setting.py:139
msgid "Malaysian"
msgstr ""
-#: application/view/setting.py:141
+#: application/view/setting.py:140
msgid "Bengali"
msgstr ""
-#: application/view/setting.py:142
+#: application/view/setting.py:141
msgid "Persian"
msgstr ""
-#: application/view/setting.py:143
+#: application/view/setting.py:142
msgid "Urdu"
msgstr ""
-#: application/view/setting.py:144
+#: application/view/setting.py:143
msgid "Swahili"
msgstr ""
-#: application/view/setting.py:145
+#: application/view/setting.py:144
msgid "Vietnamese"
msgstr ""
-#: application/view/setting.py:146
+#: application/view/setting.py:145
msgid "Punjabi"
msgstr ""
-#: application/view/setting.py:147
+#: application/view/setting.py:146
msgid "Javanese"
msgstr ""
-#: application/view/setting.py:148
+#: application/view/setting.py:147
msgid "Tagalog"
msgstr ""
-#: application/view/setting.py:149
+#: application/view/setting.py:148
msgid "Hausa"
msgstr ""
-#: application/view/share.py:50 application/view/subscribe.py:239
+#: application/view/share.py:51 application/view/subscribe.py:240
msgid "Unknown command: {}"
msgstr ""
-#: application/view/share.py:56
+#: application/view/share.py:57
msgid "There is no {} email yet."
msgstr ""
-#: application/view/share.py:96 application/view/share.py:121
-#: application/view/share.py:143
+#: application/view/share.py:105 application/view/share.py:130
+#: application/view/share.py:152
msgid "Saved to your {} account."
msgstr ""
-#: application/view/share.py:99 application/view/share.py:117
-#: application/view/share.py:146
+#: application/view/share.py:108 application/view/share.py:126
+#: application/view/share.py:155
msgid "Failed save to {}."
msgstr ""
-#: application/view/share.py:100 application/view/share.py:118
-#: application/view/share.py:147
+#: application/view/share.py:109 application/view/share.py:127
+#: application/view/share.py:156
msgid "Reason :"
msgstr ""
-#: application/view/share.py:109
+#: application/view/share.py:118
msgid "Unauthorized {} account!"
msgstr ""
-#: application/view/share.py:122
+#: application/view/share.py:131
msgid "See details below:"
msgstr ""
-#: application/view/share.py:145
+#: application/view/share.py:154
msgid "Unknown: {}"
msgstr ""
@@ -1353,41 +1358,41 @@ msgstr ""
msgid "Failed to fetch the recipe."
msgstr ""
-#: application/view/subscribe.py:123 application/view/subscribe.py:264
+#: application/view/subscribe.py:123 application/view/subscribe.py:265
msgid "Failed to save the recipe. Error:"
msgstr ""
-#: application/view/subscribe.py:220
+#: application/view/subscribe.py:221
msgid "You can only delete the uploaded recipe."
msgstr ""
-#: application/view/subscribe.py:224
+#: application/view/subscribe.py:225
msgid "The recipe have been subscribed, please unsubscribe it before delete."
msgstr ""
-#: application/view/subscribe.py:237
+#: application/view/subscribe.py:238
msgid "This recipe has not been subscribed to yet."
msgstr ""
-#: application/view/subscribe.py:251
+#: application/view/subscribe.py:252
msgid "Can not read uploaded file, Error:"
msgstr ""
-#: application/view/subscribe.py:259
+#: application/view/subscribe.py:260
msgid ""
"Failed to decode the recipe. Please ensure that your recipe is saved in "
"utf-8 encoding."
msgstr ""
-#: application/view/subscribe.py:279
+#: application/view/subscribe.py:280
msgid "The recipe is already in the library."
msgstr ""
-#: application/view/subscribe.py:309
+#: application/view/subscribe.py:310
msgid "The login information for this recipe has been cleared."
msgstr ""
-#: application/view/subscribe.py:313
+#: application/view/subscribe.py:314
msgid "The login information for this recipe has been saved."
msgstr ""
diff --git a/readme.md b/readme.md
index 4231ce4d..332702aa 100644
--- a/readme.md
+++ b/readme.md
@@ -1,75 +1,48 @@
-Readme of english version refers to [Readme_EN.md](https://github.com/cdhigh/KindleEar/blob/master/readme_EN.md)
+中文readme请参考 [Readme_zh.md](https://github.com/cdhigh/KindleEar/blob/master/readme_zh.md)
+# Announcement
+March 10, 2024
+Official release of 3.0 beta, KindleEar is ready for deployment now.
+The author has successfully deployed it to GAE/Oracle Cloud/PythonAnywhere.
-# 公告
-从 2024-01-06 起作者开始将KindleEar迁移至Python3,暂时无法评估迁移完成的最终时间,在公告完成前本仓库的代码将无法部署,如果要部署,请下载
-[v1.26.9代码存档](https://github.com/cdhigh/KindleEar/releases/tag/1.26.9)
+**Significant Updates:**
+* Supports Python 3
+* Redesigned software framework
+* Support for multiple platforms, no longer limited to the GAE platform
+* Support for Calibre's recipe format directly
+* Preset with over 1,000 Calibre builtin recipe files
-
-
-
-
-# 简介
-这是一个Kindle个人推送服务应用,可以将其部署在各种支持Python的托管平台或VPS上。
-每天自动聚合各种网络信息制作成epub/mobi格式推送至您的Kindle或其他电子书阅读器。
-
-此应用目前的主要功能有:
-
-* 支持Calibre的recipe格式的不限量RSS/ATOM或网页内容收集
-* 不限量自定义RSS,直接输入RSS/ATOM链接和标题即可自动推送
-* 多账号管理,支持多用户和多Kindle
-* 生成带图像有目录的epub/mobi
-* 自动每天定时推送
-* 内置共享库,可以直接订阅其他网友分享的订阅源,也可以分享自己的订阅源给其他网友
-* 强大而且方便的邮件中转服务
-* 和Evernote/Pocket/Instapaper等系统的集成
+# Brief Introduction
+KindleEar is a web application which can be deployed on various Python-hosting platforms or VPS.
+It automatically aggregates various web content into epub/mobi and delivers it to your Kindle or other e-book readers daily.
-# 标准部署步骤
-1. [申请google账号](https://accounts.google.com/SignUp) 并暂时 [启用不够安全的应用的访问权限](https://www.google.com/settings/security/lesssecureapps) 以便上传程序。
+## The features included:
+* Unlimited RSS/ATOM or web content collection with support for Calibre's recipe format
+* Unlimited custom RSS, directly enter RSS/ATOM link and title for automatic push
+* Multiple account management, supporting multiple users and multiple Kindles
+* Generate epub/mobi with images and table of contents
+* Automatic daily scheduled push
+* Built-in sharing library, you can directly subscribe to feeds shared by other users, and you can also share your own feeds with others
+* Powerful and convenient email forwarding service
+* Integration with systems like Evernote/Pocket/Instapaper
-2. [创建一个Application](https://console.developers.google.com/project),注意不用申请GCE,那个是60天试用的,而GAE是限额范围内永久免费的。
-3. 安装 [Python 3.x](https://www.python.org/downloads/)。
-4. 安装 [google cloud SDK/gloud CLI](https://storage.cloud.google.com/cloud-sdk-release)
+![Screenshot](https://raw.githubusercontent.com/cdhigh/KindleEar/master/docs/scrshot.gif)
-5. 下载 [KindleEar](https://github.com/cdhigh/KindleEar/archive/master.zip) ,解压到一个特定的目录。
-6. 修改config.py的一些参数,特别是前面几个,务必修改。
-7. 转到GAE SDK安装目录(默认为:*C:\Program Files\Google\google_appengine*)
+For other details, please refer to the [project documentation](https://cdhigh.github.io/KindleEar).
-8. 部署命令:
- * `gcloud auth login`
- * `gcloud config set project 你的ApplicationId`
- * `gcloud app deploy --version=1 app.yaml module-worker.yaml`
- * `gcloud app deploy --version=1 KindleEar目录`
- * [如果服务器没有正常创建数据库索引或定时任务,可能需要手动执行如下语句]
- `gcloud datastore indexes create index.yaml`
- `gcloud app deploy --version=1 app.yaml queue.yaml`
- `gcloud app deploy --version=1 app.yaml cron.yaml`
- `gcloud app deploy --version=1 app.yaml dispatch.yaml`
-9. 全部完成后就可以尝试打开域名:
-*http://appid.appspot.com* (appid是你申请的application名字)
-比如作者的网站域名为:
-**注:初始用户名为 admin, 密码为 admin,建议登录后及时修改密码。**
+# License
+ KindleEar is Licensed under the MIT license.
-10. 更详细一点的说明请参照Github上的 [FAQ](http://htmlpreview.github.io/?https://github.com/cdhigh/KindleEar/blob/master/static/faq.html) 或作者网站的 [FAQ](http://kindleear.appspot.com/static/faq.html) 链接。有关部署失败,部署后"internal server error"等问题都有解释。
-# 简化的部署步骤(推荐)
- 假如你不想安装python和GAE SDK:
- 参考代码库 和教程 。
- 这种方法直接在GAE后台的console窗口就可以实现部署。
-
-# 许可协议
-KindleEar is licensed under the [AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html) license.
-大体的许可框架是此应用代码你可以任意使用,任意修改,可以商用,但是必须将你修改后的代码开源并保留原始版权声明。
-
-# 主要贡献者
+# Contributors
* @rexdf
* @insert0003
* @zhu327
@@ -78,3 +51,6 @@ KindleEar is licensed under the [AGPLv3](http://www.gnu.org/licenses/agpl-3.0.ht
* @seff
* @miaowm5
* @bookfere
+
+
+
diff --git a/readme_EN.md b/readme_EN.md
deleted file mode 100644
index aff9d88c..00000000
--- a/readme_EN.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Announcement
-Starting from 2024-01-06, the author has begun the migration of KindleEar to Python 3. The final completion time of the migration cannot be assessed temporarily. Before the announcement is complete, the code in this repository cannot be deployed. If you need to deploy, please download the [v1.26.9 code archive](https://github.com/cdhigh/KindleEar/releases/tag/1.26.9).
-
-
-
-
-
-
-# Brief Introduction
-KindleEar is a web application to aggregate RSS for generating periodical mobi/epub file with images and send it to your kindle or your email automatically.
-
-## The features included:
-* Support calibre-like recipe file to aggress unlimited RSS or webpage.
-* Support custom RSS, only title/url are needed, don't need to program.
-* With account management, support several kindles.
-* Generate periodical mobi/epub file with images.
-* Deliver news feeds to your kindle daily automatically.
-* Built-in shared library, can share links with others and subscribe links from others.
-* Website support multi-languages.
-* Powerful and convenient mail-transfering service.
-* Integration with Evernote/Pocket/Instapaper.
-
-# Deployment
-1. [Create a Google account](https://accounts.google.com/SignUp) and [Turn on Access for less secure apps](https://www.google.com/settings/security/lesssecureapps).
-
-2. [Create an application](https://console.developers.google.com/project).
-
-3. Install [Python 2.7.x](https://www.python.org/downloads/).
-
-4. Install [GAE SDK](https://storage.cloud.google.com/cloud-sdk-release). note: choose some version before 273.0.
- [google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip)
- [google-cloud-sdk-273.0.0-darwin-x86.tar.gz](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-darwin-x86.tar.gz)
- [google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz)
- [google-cloud-sdk-273.0.0-linux-x86.tar.gz](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-linux-x86.tar.gz)
- [google-cloud-sdk-273.0.0-linux-x86_64.tar.gz](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-linux-x86_64.tar.gz)
- [google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip)
- [google-cloud-sdk-273.0.0-windows-x86.zip](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-windows-x86.zip)
- [google-cloud-sdk-273.0.0-windows-x86_64.zip](https://storage.googleapis.com/cloud-sdk-release/google-cloud-sdk-273.0.0-windows-x86_64.zip)
-
-5. [Download KindleEar](https://github.com/cdhigh/KindleEar/archive/master.zip) and uncompress it into a directory for example: *c:\kindleear*.
-
-6. Modify some variable in app.yaml/module-worker.yaml/config.py.
-
- File | To be changed | Description |
--------------------|-------------|-----------------------|
-app.yaml | application | Your Application Id |
-module-worker.yaml | application | Your Application Id |
-config.py | SRC_EMAIL | Your Gmail Address |
-config.py | DOMAIN | appid@appspot.com |
-config.py | TIMEZONE | Your timezone |
-
-> the lines 'application' and 'version' in yaml have to be commented if you will deploy it by using gcloud.
-
-7. Deployment
- * Delete the first two lines of app.yaml and module-worker.yaml [application and version]
- * `gcloud auth login`
- * `gcloud config set project YourApplicationId`
- * `gcloud app deploy --version=1 KindleEarFolder\app.yaml KindleEarFolder\module-worker.yaml`
- * `gcloud app deploy --version=1 KindleEarFolder`
- * [If some error after deployment.]
- `gcloud datastore indexes create index.yaml`
- `gcloud app deploy --version=1 app.yaml queue.yaml`
- `gcloud app deploy --version=1 app.yaml cron.yaml`
- `gcloud app deploy --version=1 app.yaml dispatch.yaml`
-
-8. After finished, you can open the website *'http://appid.appspot.com'* (appid is the name of your application),
-For example the author's site:
-**The initial username is 'admin', password is 'admin', please change the password immediately after first login.**
-
-9. More details could be found in [FAQ](http://htmlpreview.github.io/?https://github.com/cdhigh/KindleEar/blob/master/static/faq_en.html).
-
-# Deployment simplified
-If you don't want to intall GAE SDK and python, you have another choice.
-Reference code repository and tutorial (in Chinese, but you can translate it by Google).
-This method can be deployed directly in the console window of the GAE background.
-
-# License
- KindleEar is Licensed under the [AGPLv3](http://www.gnu.org/licenses/agpl-3.0.html) license.
-
-# Contributors
-* @rexdf
-* @insert0003
-* @zhu327
-* @lord63
-* @th0mass
-* @seff
-* @miaowm5
-* @bookfere
diff --git a/readme_zh.md b/readme_zh.md
new file mode 100644
index 00000000..7cc3353e
--- /dev/null
+++ b/readme_zh.md
@@ -0,0 +1,54 @@
+# 公告
+2024-03-10
+正式发布3.0 beta版,代码库已经可以用来部署,作者已经将其成功部署到GAE/Oracle Cloud/pythonAnywhere。
+
+**主要新特性:**
+* 支持Python 3
+* 重新设计的软件架构
+* 多平台支持,不再受限于gae平台
+* 支持不用修改的Calibre的recipe格式
+* 预置Calibre的一千多个recipe文件
+
+
+
+# 简介
+这是一个Kindle个人推送服务应用,可以将其部署在各种支持Python的托管平台或VPS上。
+每天自动聚合各种网络信息制作成epub/mobi格式推送至您的Kindle或其他电子书阅读器。
+
+
+此应用目前的主要功能有:
+
+* 支持Calibre的recipe格式的不限量RSS/ATOM或网页内容收集
+* 不限量自定义RSS,直接输入RSS/ATOM链接和标题即可自动推送
+* 多账号管理,支持多用户和多Kindle
+* 生成带图像有目录的epub/mobi
+* 自动每天定时推送
+* 内置共享库,可以直接订阅其他网友分享的订阅源,也可以分享自己的订阅源给其他网友
+* 强大而且方便的邮件中转服务
+* 和Evernote/Pocket/Instapaper等系统的集成
+
+
+
+![Screenshot](https://raw.githubusercontent.com/cdhigh/KindleEar/master/docs/scrshot.gif)
+
+
+
+其他细节请参考 [项目文档](https://cdhigh.github.io/KindleEar)
+
+
+
+# 许可协议
+KindleEar is licensed under the MIT License.
+大体的许可框架是此应用代码你可以任意使用,任意修改,可以商用,但是必须将你修改后的代码开源并保留原始版权声明。
+
+# 主要贡献者
+* @rexdf
+* @insert0003
+* @zhu327
+* @lord63
+* @th0mass
+* @seff
+* @miaowm5
+* @bookfere
+
+
diff --git a/requirements.txt b/requirements.txt
index e234ecc0..748eb5f9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,34 +1,32 @@
-requests>=2.0.0,<=2.99.0
-chardet>=5.0.0,<=5.99.0
-pillow>=10.0.0,<=10.99.0
-lxml>=5.0.0,<=5.99.0
-sendgrid>=6.0.0,<=6.99.0
-python-dateutil>=2.0.0,<=2.99.0
-css_parser>=1.0.0,<=1.99.0
-beautifulsoup4>=4.0.0,<=4.99.0
+requests>=2.31.0,<3.0.0
+chardet>=5.2.0,<6.0.0
+pillow>=10.2.0,<11.0.0
+lxml>=5.0.0,<6.0.0
+sendgrid>=6.11.0,<7.0.0
+mailjet_rest>=1.3.4,<2.0.0
+python-dateutil>=2.8.2,<3.0.0
+css_parser>=1.0.10,<=2.0.0
+beautifulsoup4>=4.12.2,<5.0.0
html2text>=2020.1.16
-html5lib>=1.1
+html5lib>=1.1,<2.0
#html5-parser~=0.4.0
-gunicorn
-Flask>=3.0.0,<=3.99.0
-flask-babel>=4.0.0,<=4.99.0
-six>=1.0.0,<=1.99.0
-feedparser>=6.0.0,<=6.99.0
+gunicorn>=21.2.0,<22.0.0
+Flask>=3.0.0,<4.0.0
+flask-babel>=4.0.0,<5.0.0
+six>=1.16.0,<2.0.0
+feedparser>=6.0.11,<7.0.0
+peewee>=3.1.7,<4.0.0
+flask-apscheduler>=1.13.1,<2.0.0
+redis>=4.5.0,<6.0.0
-weedata>=0.1.0,<=0.99.0
-redis>=4.5.0,<=5.99.0
-#google-cloud-datastore>=2.19.0,<=2.99.0
-#peewee>=3.0.0,<=3.99.0
-#pymysql>=1.0.0,<=1.99.0
-#psycopg2>=2.0.0,<=2.99.0
-#pymongo>=3.0.0,<=3.99.0
-
-flask-apscheduler>=1.0.0,<=1.99.0
-#google-cloud-tasks>=2.0.0,<=2.99.0
-#celery>=5.0.0,<=5.99.0
-#eventlet>=0.30.0,<=0.99.0
-#flask-rq2>=18.0,<=18.99
-
-appengine-python-standard>=1.1.0,<=1.99.0
-
-sqlalchemy
+#pymysql>=1.1.0,<2.0.0
+#psycopg2>=2.9.9,<3.0.0
+#weedata>=0.2.1,<1.0.0
+#google-cloud-datastore>=2.19.0,<3.0.0
+#pymongo>=3.7.2,<4.0.0
+#google-cloud-tasks>=2.15.0,<3.0.0
+#celery>=5.3.6,<6.0.0
+#eventlet>=0.35.1,<1.0.0
+#flask-rq2>=18.3,<19.0
+#appengine-python-standard>=1.1.6,<2.0.0
+#sqlalchemy>=2.0.28,<3.0.0
diff --git a/readme.developer.md b/tests/readme.developer.md
similarity index 98%
rename from readme.developer.md
rename to tests/readme.developer.md
index dbb15d70..8e5f5a27 100644
--- a/readme.developer.md
+++ b/tests/readme.developer.md
@@ -6,6 +6,7 @@
3. 使用命令打开调试环境
`c:\python38\python.exe "C:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\bin\dev_appserver.py" --runtime_python_path="python27=c:\python27\python.exe,python3=c:\python38\python.exe" --skip_sdk_update_check=true app.yaml worker.yaml`
`--support_datastore_emulator=true`
+ dev_appserver.py --runtime_python_path=c:\python38\python.exe --application=kindleear5 app.yaml
2. 即使在本机,GAE应用也运行在一个沙箱内,无法读写本机文件,如果要突破,可以修改 stubs.py 里面的 FakeFile 类。
* 删除__init__()
diff --git a/tests/runtests.py b/tests/runtests.py
index 63f1a5f9..d3554f4c 100644
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -27,7 +27,6 @@ def set_env():
os.environ['TASK_QUEUE_SERVICE'] = TASK_QUEUE_SERVICE
os.environ['TASK_QUEUE_BROKER_URL'] = TASK_QUEUE_BROKER_URL
os.environ['APP_DOMAIN'] = APP_DOMAIN
- os.environ['SRC_EMAIL'] = SRC_EMAIL
os.environ['ADMIN_NAME'] = ADMIN_NAME
os.environ['HIDE_MAIL_TO_LOCAL'] = '1' if HIDE_MAIL_TO_LOCAL else ''
diff --git a/tests/test_logs.py b/tests/test_logs.py
index 486e33b6..54170e95 100644
--- a/tests/test_logs.py
+++ b/tests/test_logs.py
@@ -24,7 +24,7 @@ def test_logs(self):
data['user'] = 'other'
DeliverLog.create(**data)
- KeUser.create(name='other', passwd='pwd', email='1@2')
+ KeUser.create(name='other', passwd='pwd', email='1@2', sender='1@2')
resp = self.client.get('/logs')
self.assertEqual(resp.status_code, 200)
self.assertIn('Logs of other users', resp.text)
@@ -35,7 +35,7 @@ def test_remove_logs(self):
self.assertEqual(resp.status_code, 200)
self.assertIn('lines delivery log removed.', resp.text)
- KeUser.create(name='other', passwd='pwd', email='1@1', enable_send=True, expiration_days=7,
+ KeUser.create(name='other', passwd='pwd', email='1@1', , sender='1@1', enable_send=True, expiration_days=7,
expires=datetime.datetime.utcnow() - datetime.timedelta(days=30))
resp = self.client.get('/removelogs')
self.assertEqual(resp.status_code, 200)
diff --git a/babel.cfg b/tools/babel.cfg
similarity index 100%
rename from babel.cfg
rename to tools/babel.cfg
diff --git a/tools/deploy_helper.py b/tools/deploy_helper.py
index a98fba6f..70369e28 100644
--- a/tools/deploy_helper.py
+++ b/tools/deploy_helper.py
@@ -1,49 +1,51 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
-"""帮助设置requirments.txt和config.py
+"""create/update requirments.txt of KindleEar
"""
import re, os, sys, shutil
-REQ_COMM = [('requests', '>=2.0.0,<=2.99.0'),
- ('chardet', '>=5.0.0,<=5.99.0'),
- ('pillow', '>=10.0.0,<=10.99.0'),
- ('lxml', '>=5.0.0,<=5.99.0'),
- ('sendgrid', '>=6.0.0,<=6.99.0'),
- ('python-dateutil', '>=2.0.0,<=2.99.0'),
- ('css_parser', '>=1.0.0,<=1.99.0'),
- ('beautifulsoup4', '>=4.0.0,<=4.99.0'),
+REQ_COMM = [('requests', '>=2.31.0,<3.0.0'),
+ ('chardet', '>=5.2.0,<6.0.0'),
+ ('pillow', '>=10.2.0,<11.0.0'),
+ ('lxml', '>=5.0.0,<6.0.0'),
+ ('sendgrid', '>=6.11.0,<7.0.0'),
+ ('mailjet_rest', '>=1.3.4,<2.0.0'),
+ ('python-dateutil', '>=2.8.2,<3.0.0'),
+ ('css_parser', '>=1.0.10,<=2.0.0'),
+ ('beautifulsoup4', '>=4.12.2,<5.0.0'),
('html2text', '>=2020.1.16'),
- ('html5lib', '>=1.1'),
+ ('html5lib', '>=1.1,<2.0'),
('#html5-parser', '~=0.4.0'),
- ('gunicorn', ''),
- ('Flask', '>=3.0.0,<=3.99.0'),
- ('flask-babel', '>=4.0.0,<=4.99.0'),
- ('six', '>=1.0.0,<=1.99.0'),
- ('feedparser', '>=6.0.0,<=6.99.0'),
+ ('gunicorn', '>=21.2.0,<22.0.0'),
+ ('Flask', '>=3.0.0,<4.0.0'),
+ ('flask-babel', '>=4.0.0,<5.0.0'),
+ ('six', '>=1.16.0,<2.0.0'),
+ ('feedparser', '>=6.0.11,<7.0.0'),
]
REQ_DB = {
- 'datastore': [('weedata', '>=0.1.0,<=0.99.0'), ('google-cloud-datastore', '>=2.19.0,<=2.99.0'),],
- 'sqlite': [('peewee', '>=3.0.0,<=3.99.0'),],
- 'mysql': [('peewee', '>=3.0.0,<=3.99.0'), ('pymysql', '>=1.0.0,<=1.99.0'),],
- 'postgresql': [('peewee', '>=3.0.0,<=3.99.0'), ('psycopg2', '>=2.0.0,<=2.99.0'),],
- 'cockroachdb': [('peewee', '>=3.0.0,<=3.99.0'), ('psycopg2', '>=2.0.0,<=2.99.0'),],
- 'mongodb': [('weedata', '>=0.1.0,<=0.99.0'), ('pymongo', '>=3.0.0,<=3.99.0'),],
- 'redis': [('weedata', '>=0.1.0,<=0.99.0'), ('redis', '>=4.5.0,<=5.99.0'),],
- 'pickle': [('weedata', '>=0.1.0,<=0.99.0'),],
+ 'sqlite': [('peewee', '>=3.1.7,<4.0.0'),],
+ 'mysql': [('peewee', '>=3.1.7,<4.0.0'), ('pymysql', '>=1.1.0,<2.0.0'),],
+ 'postgresql': [('peewee', '>=3.1.7,<4.0.0'), ('psycopg2', '>=2.9.9,<3.0.0'),],
+ 'cockroachdb': [('peewee', '>=3.1.7,<=4.0.0'), ('psycopg2', '>=2.9.9,<3.0.0'),],
+ 'datastore': [('weedata', '>=0.2.1,<1.0.0'), ('google-cloud-datastore', '>=2.19.0,<3.0.0'),],
+ 'mongodb': [('weedata', '>=0.2.1,<1.0.0'), ('pymongo', '>=3.7.2,<4.0.0'),],
+ 'redis': [('weedata', '>=0.2.1,<1.0.0'), ('redis', '>=4.5.0,<6.0.0'),],
+ 'pickle': [('weedata', '>=0.2.1,<1.0.0'),],
}
REQ_TASK = {
- 'gae': [('google-cloud-tasks', '>=2.0.0,<=2.99.0'),],
- 'apscheduler': [('flask-apscheduler', '>=1.0.0,<=1.99.0')],
- 'celery': [('celery', '>=5.0.0,<=5.99.0'), ('eventlet', '>=0.30.0,<=0.99.0')],
- 'rq': [('flask-rq2', '>=18.0,<=18.99'),],
+ 'gae': [('google-cloud-tasks', '>=2.15.0,<3.0.0'),],
+ 'apscheduler': [('flask-apscheduler', '>=1.13.1,<2.0.0')],
+ 'celery': [('celery', '>=5.3.6,<6.0.0'), ('eventlet', '>=0.35.1,<1.0.0')],
+ 'rq': [('flask-rq2', '>=18.3,<19.0'),],
}
-REQ_PLAT = {'gae': [('appengine-python-standard', '>=1.1.0,<=1.99.0'),],}
+REQ_PLAT = {'gae': [('appengine-python-standard', '>=1.1.6,<2.0.0'),],}
EXTRA = {
- 'sqlalchemy': [('sqlalchemy', '')],
+ 'sqlalchemy': [('sqlalchemy', '>=2.0.28,<3.0.0')],
+ 'redis': [('redis', '>=4.5.0,<6.0.0')],
}
def write_req(reqFile, db, task, plat, *extra):
@@ -51,85 +53,41 @@ def write_req(reqFile, db, task, plat, *extra):
f.write('\n'.join([''.join(item) for item in REQ_COMM]))
f.write('\n')
EXTRAS = [EXTRA for idx in range(len(extra))]
+ seen = set()
for req, opt in zip([REQ_DB, REQ_TASK, REQ_PLAT, *EXTRAS], [db, task, plat, *extra]):
- f.write('\n')
+ #f.write('\n')
items = req.get(opt, None)
- seen = set()
for item in (items or []):
if item[0] not in seen:
f.write(''.join(item) + '\n')
seen.add(item[0])
+ f.write('\n')
+ for req, opt in zip([REQ_DB, REQ_TASK, REQ_PLAT, *EXTRAS], [db, task, plat, *extra]):
+ items = req.get(opt, None)
for key, items in req.items():
- if key != opt:
- for item in filter(lambda x: x[0] not in seen, (items or [])):
- seen.add(item[0])
- f.write('#' + ''.join(item) + '\n')
+ for item in filter(lambda x: x[0] not in seen, (items or [])):
+ seen.add(item[0])
+ f.write('#' + ''.join(item) + '\n')
#parse config.py to a string with format symbols
-def config_to_fmtstr(cfgFile, fmt='dict'):
+def config_to_dict(cfgFile):
with open(cfgFile, 'r', encoding='utf-8') as f:
- lines = f.read().splitlines()
-
- ret = [] if fmt == 'list' else {}
- docComment = False
- pattern = r"""^([_A-Z]+)\s*=\s*([f]{0,1}"[^"]*"|[f]{0,1}'[^']*'|\S+)\s*(#.*)?$"""
- for line in lines:
- line = line.strip()
- if line.startswith(('"""', "'''")):
- docComment = not docComment
- if fmt == 'list':
- ret.append(line)
- continue
- elif not line or line.startswith('#') or docComment:
- if fmt == 'list':
- ret.append(line)
- continue
-
- match = re.match(pattern, line)
- if match:
- if fmt == 'list':
- ret.append((match.group(1), match.group(2), match.group(3)))
- else:
- ret[match.group(1)] = match.group(2).strip('f"\'')
- else:
- ret.append(line)
- return ret
-
-#Write to config.py, cfgItems={'APPID':,...}
-def write_cfg(cfgFile, cfgItems):
- dir_ = os.path.dirname(cfgFile)
- b, ext = os.path.splitext(os.path.basename(cfgFile))
- bakFile = os.path.join(dir_, f'{b}_bak{ext}')
- try:
- shutil.copy(cfgFile, bakFile)
- except Exception as e:
- print(str(e))
- return
-
- cfg = config_to_fmtstr(cfgFile)
- with open(cfgFile, 'w', encoding='utf-8') as f:
- for line in cfg:
- if not isinstance(line, tuple):
- f.write(line)
- f.write('\n')
- continue
-
- item, orgValue, comment = line
- comment = (' ' + comment) if comment else ''
- value = cfgItems.get(item, None)
- if value is not None:
- value = f'"{value}"' if isinstance(value, str) else value
- f.write(f'{item} = {value}{comment}\n')
- else:
- f.write(f'{item} = {orgValue}{comment}\n')
+ code = compile(f.read(), cfgFile, 'exec')
+ config_dict = {}
+ exec(code, globals(), config_dict)
+ return config_dict
if __name__ == '__main__':
print('\nThis script can help you to generate requirements.txt.\n')
- thisDir = os.path.dirname(__file__)
- cfgFile = os.path.join(thisDir, '..', 'config.py')
- reqFile = os.path.join(thisDir, '..', 'requirements.txt')
+ usrInput = input('Press y to continue :')
+ if usrInput.lower() != 'y':
+ sys.exit(0)
- cfg = config_to_fmtstr(cfgFile)
+ thisDir = os.path.abspath(os.path.dirname(__file__))
+ cfgFile = os.path.normpath(os.path.join(thisDir, '..', 'config.py'))
+ reqFile = os.path.normpath(os.path.join(thisDir, '..', 'requirements.txt'))
+
+ cfg = config_to_dict(cfgFile)
db = cfg['DATABASE_URL'].split('://')[0]
task = cfg['TASK_QUEUE_SERVICE']
broker = cfg['TASK_QUEUE_BROKER_URL']
@@ -146,4 +104,4 @@ def write_cfg(cfgFile, cfgItems):
elif broker.startswith(('sqlite://', 'mysql://', 'postgresql://')):
extras.add('sqlalchemy')
write_req(reqFile, db, task, plat, *extras)
-
+ print(f'Finished create {reqFile}')
diff --git a/tools/nginx/default b/tools/nginx/default
new file mode 100644
index 00000000..5811a022
--- /dev/null
+++ b/tools/nginx/default
@@ -0,0 +1,105 @@
+##
+# You should look at the following URL's in order to grasp a solid understanding
+# of Nginx configuration files in order to fully unleash the power of Nginx.
+# https://www.nginx.com/resources/wiki/start/
+# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
+# https://wiki.debian.org/Nginx/DirectoryStructure
+#
+# In most cases, administrators will remove this file from sites-enabled/ and
+# leave it as reference inside of sites-available where it will continue to be
+# updated by the nginx packaging team.
+#
+# This file will automatically load configuration files provided by other
+# applications, such as Drupal or Wordpress. These applications will be made
+# available underneath a path with that package name, such as /drupal8.
+#
+# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
+##
+
+# Default server configuration
+#
+server {
+ listen 80 default_server;
+ listen [::]:80 default_server;
+ charset utf-8;
+ client_max_body_size 32M;
+
+ # SSL configuration
+ #
+ # listen 443 ssl default_server;
+ # listen [::]:443 ssl default_server;
+ #
+ # Note: You should disable gzip for SSL traffic.
+ # See: https://bugs.debian.org/773332
+ #
+ # Read up on ssl_ciphers to ensure a secure configuration.
+ # See: https://bugs.debian.org/765782
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ # include snippets/snakeoil.conf;
+
+ root /var/www/html;
+
+ # Add index.php to the list if you are using PHP
+ index index.html index.htm index.nginx-debian.html;
+
+ server_name _;
+
+ location /static/ {
+ alias /home/ubuntu/site/kindleear/application/static/;
+ }
+ location /images/ {
+ alias /home/ubuntu/site/kindleear/application/images/;
+ }
+ location = /favicon.ico {
+ alias /home/ubuntu/site/kindleear/application/static/favicon.ico;
+ }
+ location = /robots.ico {
+ alias /home/ubuntu/site/kindleear/application/static/robots.ico;
+ }
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # pass PHP scripts to FastCGI server
+ #
+ #location ~ \.php$ {
+ # include snippets/fastcgi-php.conf;
+ #
+ # # With php-fpm (or other unix sockets):
+ # fastcgi_pass unix:/run/php/php7.4-fpm.sock;
+ # # With php-cgi (or other tcp sockets):
+ # fastcgi_pass 127.0.0.1:9000;
+ #}
+
+ # deny access to .htaccess files, if Apache's document root
+ # concurs with nginx's one
+ #
+ #location ~ /\.ht {
+ # deny all;
+ #}
+}
+
+
+# Virtual Host configuration for example.com
+#
+# You can move that to a different file under sites-available/ and symlink that
+# to sites-enabled/ to enable it.
+#
+#server {
+# listen 80;
+# listen [::]:80;
+#
+# server_name example.com;
+#
+# root /var/www/example.com;
+# index index.html;
+#
+# location / {
+# try_files $uri $uri/ =404;
+# }
+#}
diff --git a/tools/nginx/gunicorn.conf.py b/tools/nginx/gunicorn.conf.py
new file mode 100644
index 00000000..d632fcf6
--- /dev/null
+++ b/tools/nginx/gunicorn.conf.py
@@ -0,0 +1,10 @@
+# gunicorn.conf.py
+pythonpath = "/home/ubuntu/.local/lib/python3.10/site-packages"
+bind = "127.0.0.1:8000"
+workers = 2
+accesslog = "/home/ubuntu/log/gunicorn.access.log"
+errorlog = "/home/ubuntu/log/gunicorn.error.log"
+capture_output = True
+loglevel = "info"
+#certfile = 'cert.pem'
+#keyfile = 'key.pem'
diff --git a/tools/nginx/gunicorn.service b/tools/nginx/gunicorn.service
new file mode 100644
index 00000000..0fe92594
--- /dev/null
+++ b/tools/nginx/gunicorn.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=gunicorn daemon
+After=syslog.target network.target remote-fs.target nss-lookup.target
+[Service]
+# your username of system
+User=ubuntu
+WorkingDirectory=/home/ubuntu/site/kindleear
+ExecStart=/home/ubuntu/.local/bin/gunicorn -c /home/ubuntu/site/kindleear/tools/nginx/gunicorn.conf.py main:app
+Restart=on-failure
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/tools/pybabel_extract.bat b/tools/pybabel_extract.bat
index 116cbfe8..eac7a574 100644
--- a/tools/pybabel_extract.bat
+++ b/tools/pybabel_extract.bat
@@ -1,4 +1,4 @@
D:
cd D:\Programer\Project\KindleEar
-pybabel extract -F babel.cfg --ignore-dirs lib --ignore-dirs tests -o messages.pot .
+pybabel extract -F tools\babel.cfg --ignore-dirs lib --ignore-dirs tests -o messages.pot .
pause
diff --git a/upload.sh b/upload.sh
deleted file mode 100755
index 60c19994..00000000
--- a/upload.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-python helper.py
-appcfg.py update ./app.yaml ./module-worker.yaml
-appcfg.py update .