From b88205893da4a9da337944540b57bd0fbaceaf22 Mon Sep 17 00:00:00 2001 From: cdhigh Date: Tue, 27 Feb 2024 22:46:07 -0300 Subject: [PATCH] porting to python3 --- .gitignore | 3 +- application/__init__.py | 1 + application/back_end/db_models.py | 33 +-- application/back_end/db_models_nosql.py | 31 +-- application/back_end/db_models_sql.py | 31 +-- application/back_end/send_mail_adpt.py | 4 +- .../back_end/task_queue_apscheduler.py | 31 ++- application/back_end/task_queue_celery.py | 39 ++- application/back_end/task_queue_rq.py | 3 +- application/lib/smtp_mail.py | 4 +- application/static/base.css | 6 +- application/static/iconfont.ttf | Bin 4396 -> 4620 bytes application/static/iconfont.woff | Bin 3052 -> 3220 bytes application/templates/admin.html | 23 +- application/templates/setting.html | 7 + .../tr_TR/LC_MESSAGES/messages.mo | Bin 20886 -> 20941 bytes .../tr_TR/LC_MESSAGES/messages.po | 176 ++++++------- .../translations/zh/LC_MESSAGES/messages.mo | Bin 19755 -> 19802 bytes .../translations/zh/LC_MESSAGES/messages.po | 176 ++++++------- application/utils.py | 18 +- application/view/admin.py | 21 +- application/view/adv.py | 28 ++- application/view/login.py | 30 ++- config.py | 27 +- docs/Chinese/2.deployment.md | 224 ++++++++++++++++- docs/Chinese/3.faq.md | 1 + docs/English/1.intro.md | 7 +- docs/English/2.deployment.md | 238 +++++++++++++++++- docs/English/3.faq.md | 1 + main.py | 15 +- messages.pot | 176 ++++++------- requirements.txt | 13 +- tests/runtests.py | 65 ++--- tools/deploy_helper.py | 63 ++++- 34 files changed, 1007 insertions(+), 488 deletions(-) diff --git a/.gitignore b/.gitignore index acb2a253..77188e44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ *.mobi *.epub *.azw3 - +*.pkl +*.bak tests/tools/* tests/rss/* tests/debug_mail/* diff --git a/application/__init__.py b/application/__init__.py index 4a89b739..8ace4219 100644 --- a/application/__init__.py +++ b/application/__init__.py @@ -10,6 +10,7 @@ from flask import Flask, render_template, session, request, g from flask_babel import Babel, gettext builtins.__dict__['_'] = gettext +builtins.__dict__['appVer'] = __Version__ #创建并初始化Flask wsgi对象 def init_app(name, debug=False): diff --git a/application/back_end/db_models.py b/application/back_end/db_models.py index 1b89227d..8fd49681 100644 --- a/application/back_end/db_models.py +++ b/application/back_end/db_models.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- #数据库结构定义,使用这个文件隔离sql和nosql的差异,尽量向外提供一致的接口 -#Visit https://github.com/cdhigh/KindleEar for the latest version +#Visit for the latest version #Author: # cdhigh import os, sys, random from operator import attrgetter from ..utils import ke_encrypt, ke_decrypt, tz_now -if os.getenv('DATABASE_ENGINE') in ("datastore", "mongodb", "redis"): +if os.getenv('DATABASE_URL').startswith(("datastore", "mongodb", "redis", "pickle")): from .db_models_nosql import * else: from .db_models_sql import * @@ -187,8 +187,8 @@ class WhiteList(MyBaseModel): #Shared RSS links from other users [for kindleear.appspot.com only] class SharedRss(MyBaseModel): - title = CharField() - url = CharField(default='') + title = CharField(index=True) + url = CharField(default='', index=True) isfulltext = BooleanField(default=False) language = CharField(default='') category = CharField(default='') @@ -239,24 +239,11 @@ def set_value(cls, name, value): cls.replace(name=name, value=value).execute() #创建数据库表格,一个数据库只需要创建一次 -#如果是sql数据库,可以使用force=True删掉之前的数据库文件(小心) -def create_database_tables(force=False): - engine = os.getenv('DATABASE_ENGINE') - #if engine == "sqlite" and dbName: - # if not force and os.path.exists(dbName): - # #print(f'[Error] Database "{dbName}" already exists') - # return - # elif os.path.exists(dbName): - # try: - # os.remove(dbName) - # except: - # pass - - if engine not in ["datastore", "mongodb"]: - #with dbInstance.connection_context(): - #connect_database() - dbInstance.create_tables([KeUser, UserBlob, Recipe, BookedRecipe, DeliverLog, WhiteList, - SharedRss, SharedRssCategory, LastDelivered, AppInfo], safe=True) - #close_database() +def create_database_tables(): + #with dbInstance.connection_context(): + #connect_database() + dbInstance.create_tables([KeUser, UserBlob, Recipe, BookedRecipe, DeliverLog, WhiteList, + SharedRss, SharedRssCategory, LastDelivered, AppInfo], safe=True) + #close_database() #print(f'Create database "{dbName}" finished') diff --git a/application/back_end/db_models_nosql.py b/application/back_end/db_models_nosql.py index 0fd1eaab..b1b041ea 100644 --- a/application/back_end/db_models_nosql.py +++ b/application/back_end/db_models_nosql.py @@ -6,28 +6,19 @@ import os, json, datetime from weedata import * -__DB_ENGINE = os.getenv('DATABASE_ENGINE') -__DB_NAME = os.getenv('DATABASE_NAME', '') -__APP_ID = os.getenv('APP_ID', 'kindleear') +dbUrl = os.getenv('DATABASE_URL') +appId = os.getenv('APP_ID', 'kindleear') -if __DB_NAME.startswith('mongodb://'): - dbInstance = MongoDbClient(__APP_ID, __DB_NAME) -elif __DB_NAME.startswith('redis://'): - dbInstance = RedisDbClient(__APP_ID, __DB_NAME) -elif __DB_ENGINE == "datastore": - dbInstance = DatastoreClient(project=__APP_ID) -elif __DB_ENGINE == "mongodb": - dbInstance = MongoDbClient(__APP_ID, host=os.getenv('DATABASE_HOST'), port=int(os.getenv('DATABASE_PORT')), - username=(os.getenv('DATABASE_USERNAME') or None), password=(os.getenv('DATABASE_PASSWORD') or None)) -elif __DB_ENGINE == "redis": - try: - db_no = int(os.getenv('DATABASE_NAME') or '0') - except: - db_no = 0 - dbInstance = RedisDbClient(__APP_ID, host=os.getenv('DATABASE_HOST'), port=int(os.getenv('DATABASE_PORT')), - db=db_no, password=(os.getenv('DATABASE_PASSWORD') or None)) +if dbUrl.startswith('mongodb://'): + dbInstance = MongoDbClient(appId, dbUrl) +elif dbUrl.startswith('redis://'): + dbInstance = RedisDbClient(appId, dbUrl) +elif dbUrl.startswith("datastore"): + dbInstance = DatastoreClient(project=appId) +elif dbUrl.startswith("pickle://"): + dbInstance = PickleDbClient(dbUrl) else: - raise Exception("database engine '{}' not supported yet".format(__DB_ENGINE)) + raise ValueError("database engine '{}' not supported yet".format(dbUrl.split(":", 1)[0])) #调用此函数正式连接到数据库(打开数据库) def connect_database(): diff --git a/application/back_end/db_models_sql.py b/application/back_end/db_models_sql.py index f6fea528..7cb1c4f6 100644 --- a/application/back_end/db_models_sql.py +++ b/application/back_end/db_models_sql.py @@ -10,33 +10,8 @@ #用于在数据库结构升级后的兼容设计,数据库结构和前一版本不兼容则需要升级此版本号 DB_VERSION = 1 - -__DB_ENGINE = os.getenv('DATABASE_ENGINE') -__DB_NAME = os.getenv('DATABASE_NAME', '') -__DB_USERNAME = os.getenv('DATABASE_USERNAME') or None -__DB_PASSWORD = os.getenv('DATABASE_PASSWORD') or None -__DB_HOST = os.getenv('DATABASE_HOST') -__DB_PORT = int(os.getenv('DATABASE_PORT')) - -dbName = '' -if '://' in __DB_NAME: - dbInstance = connect(__DB_NAME) - __DB_ENGINE = __DB_NAME.split('://', 1)[0] -elif __DB_ENGINE == "sqlite": - thisDir = os.path.dirname(os.path.abspath(__file__)) - dbName = os.path.normpath(os.path.join(thisDir, "..", "..", __DB_NAME)) if __DB_NAME != ':memory:' else __DB_NAME - dbInstance = SqliteDatabase(dbName, check_same_thread=False) -elif __DB_ENGINE == "mysql": - dbInstance = MySQLDatabase(__DB_NAME, user=__DB_USERNAME, password=__DB_PASSWORD, - host=__DB_HOST, port=__DB_PORT) -elif __DB_ENGINE == "postgresql": - dbInstance = PostgresqlDatabase(__DB_NAME, user=__DB_USERNAME, password=__DB_PASSWORD, - host=__DB_HOST, port=__DB_PORT) -elif __DB_ENGINE == "cockroachdb": - dbInstance = CockroachDatabase(__DB_NAME, user=__DB_USERNAME, password=__DB_PASSWORD, - host=__DB_HOST, port=__DB_PORT) -else: - raise Exception("database engine '{}' not supported yet".format(__DB_ENGINE)) +dbName = os.getenv('DATABASE_URL') +dbInstance = connect(dbName) #调用此函数正式连接到数据库(打开数据库) def connect_database(): @@ -46,7 +21,7 @@ def connect_database(): #关闭数据库连接 def close_database(): global dbInstance - if not dbInstance.is_closed() and dbName != ':memory:': + if not dbInstance.is_closed() and dbName != 'sqlite://:memory:': dbInstance.close() #自定义字段,在本应用用来保存列表 diff --git a/application/back_end/send_mail_adpt.py b/application/back_end/send_mail_adpt.py index aae52c0c..ad1c5401 100644 --- a/application/back_end/send_mail_adpt.py +++ b/application/back_end/send_mail_adpt.py @@ -30,13 +30,15 @@ #返回当前可用的发送邮件服务列表 def avaliable_sm_services(): - sm = ['local'] + sm = [] if gae_mail: sm.append('gae') if SendGridAPIClient: sm.append('sendgrid') if smtp_send_mail: sm.append('smtp') + if not os.getenv('HIDE_MAIL_TO_LOCAL'): + sm.append('local') return sm #发送邮件 diff --git a/application/back_end/task_queue_apscheduler.py b/application/back_end/task_queue_apscheduler.py index b2346a7f..022e3b48 100644 --- a/application/back_end/task_queue_apscheduler.py +++ b/application/back_end/task_queue_apscheduler.py @@ -2,27 +2,42 @@ # -*- coding:utf-8 -*- #任务队列APScheduler #Author: cdhigh -import random +import os, random from flask_apscheduler import APScheduler -#from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, EVENT_JOB_MISSED +from apscheduler.schedulers.background import BackgroundScheduler -scheduler = APScheduler() +_broker_url = os.getenv('TASK_QUEUE_BROKER_URL') +if _broker_url.startswith('redis://'): + import redis + from apscheduler.jobstores.redis import RedisJobStore + _client = RedisJobStore() + _client.redis = redis.from_url(_broker_url) + jobstores = {"default": _client} +elif _broker_url.startswith('mongodb://'): + import pymongo + from apscheduler.jobstores.mongodb import MongoDBJobStore + _client = pymongo.MongoClient(_broker_url) + jobstores = {"default": MongoDBJobStore(client=_client)} +elif _broker_url.startswith(('sqlite://', 'mysql://', 'postgresql://')): + from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore + jobstores = {"default": SQLAlchemyJobStore(url=_broker_url)} +elif _broker_url == '': + jobstores = None #default is memory store +else: + raise ValueError('Unsupported TASK_QUEUE_BROKER_URL type: {_broker_url}') + +scheduler = APScheduler(scheduler=BackgroundScheduler(jobstores=jobstores)) #https://viniciuschiele.github.io/flask-apscheduler/rst/api.html scheduler.api_enabled = True #提供/scheduler/jobs等几个有用的url def init_task_queue_service(app): scheduler.init_app(app) - #scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED) scheduler.start() app.extensions["scheduler"] = scheduler return scheduler -#APScheduler会自动删除trigger为date的任务,这个函数不需要了 -#def job_listener(event): -# scheduler.remove_job(event.job_id) - #@scheduler.task('interval', id='check_deliver', hours=1, misfire_grace_time=20*60, coalesce=True) @scheduler.task('cron', minute=50, id='check_deliver', misfire_grace_time=20*60, coalesce=True) def check_deliver(): diff --git a/application/back_end/task_queue_celery.py b/application/back_end/task_queue_celery.py index 1a87917e..9efaeaff 100644 --- a/application/back_end/task_queue_celery.py +++ b/application/back_end/task_queue_celery.py @@ -6,6 +6,7 @@ #启动celery #celery -A main.celery_app worker --loglevel=info --logfile=d:\celery.log --concurrency=2 -P eventlet #celery -A main.celery_app beat -s /home/celery/var/run/celerybeat-schedule --loglevel=info --logfile=d:\celery.log --concurrency=2 -P eventlet +import os from celery import Celery, Task, shared_task from celery.schedules import crontab @@ -14,10 +15,40 @@ class FlaskTask(Task): def __call__(self, *args, **kwargs): with app.app_context(): return self.run(*args, **kwargs) - + + broker_url = app.config['TASK_QUEUE_BROKER_URL'] + backend_url = broker_url + transport_opts = {} + if broker_url.startswith(('sqlite://', 'mysql://', 'postgresql://')): + broker_url = f'sqla+{broker_url}' + elif broker_url.startswith('file://'): #using a filesystem, ensure the folder exists + if broker_url.startswith('file:////?/'): #windows + dir_ = broker_url[11:] + elif broker_url.startswith('file:///'): #linux/mac + dir_ = broker_url[8:] + else: + raise ValueError('The value of TASK_QUEUE_BROKER_URL is invalid') + dir_in = os.path.join(dir_, 'data_in') + dir_out = os.path.join(dir_, 'data_out') + dir_procsed = os.path.join(dir_, 'processed') + transport_opts = {'data_folder_in': dir_in, 'data_folder_out': dir_out, 'processed_folder': dir_procsed, + 'store_processed': True} + for d in [dir_, dir_in, dir_out, dir_procsed]: + if not os.path.exists(d): + os.makedirs(d) + broker_url = 'filesystem://' + + if backend_url.startswith(('sqlite://', 'mysql://', 'postgresql://')): + backend_url = f'db+{backend_url}' + app.config.from_mapping( - CELERY={'broker_url': app.config['TASK_QUEUE_BROKER_URL'], - 'result_backend': app.config['TASK_QUEUE_RESULT_BACKEND'], + CELERY={'broker_url': broker_url, + 'result_backend': backend_url, + 'mongodb_backend_settings': { + 'database': 'kindleear', + 'taskmeta_collection': 'kindleear_taskqueue', + }, + 'broker_transport_options': transport_opts, 'task_ignore_result': True, },) @@ -28,7 +59,7 @@ def __call__(self, *args, **kwargs): celery_app.conf.beat_schedule = { 'check_deliver': { 'task': 'check_deliver', - 'schedule': crontab(minute=0, hour='*/1'), #每个小时 + 'schedule': crontab(minute=50, hour='*/1'), #每个小时 'args': [] }, 'remove_logs': { diff --git a/application/back_end/task_queue_rq.py b/application/back_end/task_queue_rq.py index 256e59ab..788806f7 100644 --- a/application/back_end/task_queue_rq.py +++ b/application/back_end/task_queue_rq.py @@ -15,8 +15,9 @@ def init_task_queue_service(app): app.config['RQ_REDIS_URL'] = app.config['TASK_QUEUE_BROKER_URL'] rq.init_app(app) + #windows不支持,暂时屏蔽,正式版本需要取消注释 #check_deliver.cron('0 */1 * * *', 'check_deliver') #每隔一个小时执行一次 - #remove_logs.cron('0 0 */1 * *', 'check_deliver') #每隔24小时执行一次 + #remove_logs.cron('0 0 */1 * *', 'remove_logs') #每隔24小时执行一次 return rq @rq.job diff --git a/application/lib/smtp_mail.py b/application/lib/smtp_mail.py index f87e8849..f0cf9e91 100644 --- a/application/lib/smtp_mail.py +++ b/application/lib/smtp_mail.py @@ -14,7 +14,7 @@ def smtp_send_mail(sender, to, subject, body, host, username, password, port=Non host, port = host.split(':', 2) port = int(port) else: - port = 25 + port = 587 #587-TLS, 465-SSL to = to if isinstance(to, list) else [to] message = MIMEMultipart('alternative') if html else MIMEMultipart() @@ -32,7 +32,7 @@ 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_SSL(host=host, port=port) as smtp_server: + with smtplib.SMTP(host=host, port=port) as smtp_server: smtp_server.connect(host, port) smtp_server.ehlo() smtp_server.starttls() diff --git a/application/static/base.css b/application/static/base.css index b5a09297..c720311e 100644 --- a/application/static/base.css +++ b/application/static/base.css @@ -797,6 +797,9 @@ div[class="schedule_daytimes"] input { .icon-schedule:before { content: "\ea5f"; } +.icon-edit:before { + content: "\e60c"; +} /* upload cover images */ .imgFileUploade{ @@ -869,10 +872,11 @@ div[class="schedule_daytimes"] input { bottom: 20px; left: 50%; transform: translateX(-50%); - background-color: #ffe135; + background-color: #fcfab2; color: #333; padding: 15px 20px; border-radius: 10px; + border: 1px solid #ada802; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); z-index: 9999; } diff --git a/application/static/iconfont.ttf b/application/static/iconfont.ttf index b83e03fa879717c61effaea7b7d098a44678925a..4b39cfad48de45dee0e417ec1f641dae35e6e77b 100644 GIT binary patch delta 808 zcmY*VO-vI}5dP-v>y~aymjVk4W&@NIQ-6b{1!>Abyr>C18v{0g6hom%O=^s(dO|}C zxE%C~UNppD^kj(P;MJ?~h#poW9z3WA6N>Y?8solv@69(m^Uar8?^^0!j#dT$S^{u! zypSutFP(J((ak7M&0M?Gkz9EK$d`a|EI*wI&6D9Q(^ZE= z?~ymPEbtmyEjrAv!KH5?p`5p&L1kRH0XO7~igpS)WpQXwskQxg4&d+M)$mk;jsSez zU5^lg@UtR2A>H{PemUPnpb}E1uwJ{tRp3R}pd7dX53B^2D;1POx7`D)!;O33IdG49 z;E8aDJ@9O}r#;m4dF!s)JZpU#sHlrckl$?&Y!aWhJg{AS8T0UGg`+e~m+2|}6iZs8 zc1By1wp^CqeKA(y>b)%q?H1Z`3<>nJ+b-RTbVfRRlZjZgQ<3JD6U_P|iC9xltkUHt z-6Y)>LzGNWGC}?nwWj<%DeZ!3?PjTLT9#Q|wBvDGY}j$@X4Xnu)di`IOIhD!?C6s2 zS=Hlr2dO1HpUGxuk;@;84a>IT_HI`75i8N^h{bI_eaC1FOF1D_Te^DQIC+O!N9Hpl p8N7HP_lKfk5~PtNAL-<$Itq|MK?+elg~^1RoR}^-ZyUx4zX8m`mtOz? delta 585 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 delta 2575 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
-
-
+ @@ -82,7 +82,7 @@ {{u.expiration_days}} Days {% endif -%} - + {% if u.name == adminName -%} {% else -%} @@ -97,3 +97,18 @@ {% endif -%}
{% endblock -%} +{% block js -%} + +{% endblock -%} \ No newline at end of file diff --git a/application/templates/setting.html b/application/templates/setting.html index 0ef12c2d..c7978fa5 100644 --- a/application/templates/setting.html +++ b/application/templates/setting.html @@ -274,6 +274,13 @@ $('#sm_username').hide(); $('#sm_password').hide(); $('#sm_save_path').show(); + } else { + $('#sm_apikey').hide(); + $('#sm_host').hide(); + $('#sm_port').hide(); + $('#sm_username').hide(); + $('#sm_password').hide(); + $('#sm_save_path').hide(); } } diff --git a/application/translations/tr_TR/LC_MESSAGES/messages.mo b/application/translations/tr_TR/LC_MESSAGES/messages.mo index e26588a08f9f5267394497857901cf23e51db836..a337ad960080e6cd6c19297e819b8ee9a23b8d46 100644 GIT binary patch delta 5195 zcmYM%3s6m7dG9e&QGn80m@KLviqB;w!$s{Q}ZnOI|nQSHWH{1k)mg7u1Rzh>(aAU?B$CzRYqiOKr!`Olmcn~%4F;r5{V<>)UpI^7__b`+8;1O;> zUJRsOfSRxf^?oU8Ay1&@tHB`VH%+!-BPLPbi(~LK=Hd-Z#nB_l5|*G=UXNPw3mAnj zVV+0$oy|3jjwKrih_Mj%bfy&S= z)Jj8GmJVwaD$rO|dm^fTigm0t2X#jB9b1@+6*Npoy|@{}aT{vlov1T#0$;^zsFXHv zVmThB36;U^s4d%#3h-Ujicg{f`>VANl_BQ}1+roWP!or8GSo2|IoKv1H9#(EC6A!` z6{1$~D00wSX1YM)aq?9yR_Zx7{%<6cl+IDxw3ZfR3V8(u*4SYt+O8 zr~z&v|J}bbdOwM)qd>EfT{M$08!J)cy@G|G_LK8%k}#6_%_9`1V+ktK4XBCR zQ5o5TO6j|(lpjH50mZ1}LCG z5zau>%TX(whuY&B`+O~qqrMT9%EQ*bVF2|3)WCz*A=DNHa&>-;38?WG;4FM5mHg|= z)k%X6&HJbc&tWOvLanT54BLctsCozLaCPHk9Ks}=m}ZO@=ixNmfO>uc^Kl3@k2l@@ zl$Sdc3TRl3dTT4#R#7#s8pg#XZzv z3uRDkMI!oR9zM_fW)cMryc2_Q7wUQ(Kz&#~#xT5Ky^2B9Z(<&Pj~>jnXSVnyb zYU2Hwr+dl2RJXc=&~L|UBgrMc|Yp<-9%mISck8IBKKk%R-;~c85LnWDg$Rw1Am6G z_&LVlpsfdUS3T4dP}gi4DxfOlhMR@x!F8y>J8?KVdnrUw_yBdt&Z1U)0mJa$);sq3 zFg|A<+M`f=n~OR_`KZG=#afCf)T>eBy@cA*W?SEZEXXkjDJT_3P}k}_D)K>OPIDLa zJ@E4VRR-pu_IN&Oz#7y7eCR_5FXMGo|DGvsfafuT`X#KyU=G4hb^jMoP%7WSC3p(s zF^liA0x3aFRDl}sY1E4AP#LR71+)g0+E=XWZToLf{a!<5Xa~;6{TRXgCXinW?L`c7 za7_m4v@XK8aT97Qyqr8`st~o}pP}yc&+&eWQ2{oi0@{w+qPI}}I`LubLM`kp-oO9n zC@2FLQ4?La9qytc_b+q}$8pq0peCAu%0xLTfJ)T+OHrBdp)%KC+t=HA3u?g~h2(!M zh4<`(FHi#yT7N)A8amy*715~cIT5wOV$=k)ktCUg=)pHoDgGmJd`wi4`_K3iR7SR; z=4mZ*+=fmXG|?YW*X)vw+xiw%h7X|v zJ7L=s9DWfr@if%HGf^q6vhCHVl-8hDT8G-pmoNc0q55~?XxxL!&>7U0^q~U(*6KIY zoo^&+Va_ND`krq?KhuX4!jMV)P{E7Rc5Q`Z+s6f6Q<^|LqA4GkKx-lCAOWY4q9u`rbi;rP5rsF@X z!%E%Mk4FVI6LlDUs0{DM0_HbIDQM-lQF|IY+YKZK6~I%NiVeu`hH1qK*kko8bN4E!DM_51MwXU#UrTO@DYaLXBdM2M5Xi_ z)M2}4jhgFz%9By!6yQcILT%kC)cf6Y$$tigUK*5=A5as;mAeC`pgQIw2iO#%Qrv`^ zcn>P|M^W#GSGX%pMjgs&7=-1hLq5;8FGppjv4Z?ZQrJR67`9QHB+0k!fL)Jk`w-aBCHM{yYS5AlBL(M<5nTHMIr<-Yo^ld-vp(Z2dx&r;vA zTF)ysha delta 5121 zcmYM%4OG|F9mnxYJQ>e|iV(t|XB1N)5F!hieU7|kWiv^I5T>3>F^Qkxv8 zG+SDy+}KoN8=iC;wQ6(XLpx(r51R={rFrB`IPQ!d9D9HGea~6Pfb1(r8$P z85LL^YQi0;_Zm@WY(Hw=6JyE03hgv#;)~YtGjjm>!bQraglc<3&peF7{ z4R96t<8Jbo-jC+$D9~hN_gofE!kMV?*5P#AfjQWY6}tZe6tt2VKJQS>MNPCAHQ;iq zuR^8td#C_5qEi1;)JpfDGIjuU2#=%A)>*4}A<1?9sKXo0zD6;>%b`$)#i&R(q9$%Y zWn?cZrA??n51}&h8+(2nbq!CU-v1kF0ez?dZrSrtzB)QHv8Vu((XS496g1&%)XEm3 zQvVIqp4XsOz6LpW?kA{$UPeuH0Cg=7S^Yff{Y$8RH&B5OVkX{3&6l1|{xv{uy0?M? zRDBw1g>#VYb(Qw~G0dgD36;tN<_8!^y&E;~C*}>*7Tv;W7@pycKOg5%Uz|by_2qhr z1|6Da)P$$79Iv8QmPgnnxB^vw0d=_Auo!P3$H--5Iah#la4K#@JwJj)cmp+0TDJEo zpXR4<9}P9A2e0E?Jb{eq0ywH{pUXxCHXm7x`@Xe5kNMQ!LQULl&*O-aEW2W?#1*Ks z@oQ9|AEFQay%e-pLHvZN!zfgWC*c%aVf6+Kq~3->_&#cHPvV`}fx&nQ!|)2~R`j6` z+n1=Vh+yzLFdb`|-(^wIz>OG+yHVF;KkCDB6vOe1c@aaX_u@Tx9etQQ(L3#BxPbbD zsEPNX7S@8gP3@=!T*L_7|4S4`(C|6xHVjxj_-=2aC=8>0H0l=kPy^Tgqo4trIY~;*G1LHUsP;cv{V!I(fa=#{_0PVzPzxw2ApaU@rgfZ$G1Qk>hiYrDMSfY`22@~;sDbuk1|C6e zL8m?M!G+YXA=!6@9F$V5#tFC&N8+h_$iF@$|DYiP2T=nB7kV8c(MLT6^#PfITFDmk z8B{8pU@r-%Vp7)}U_CC~> zMe$kFA&NsC%6Kyi)2SDs#(N00rL|Un3bmkJNGAMlF9ltvW2neGkU3l@>N^m_cULcr zM{P|ZYQU+e6;$Bk_$}dZQ3Fmxt+*VOu|=qWmZ4Jnuvug6Yf$~3KxODDT!uSQ*YXOEW`1{*0teNF@Qb0- zdN2M8SEIHfhLfj5lZ0CFeW(Ez;&6&l0oI}d+JM@kEvSBVI1U?73u{3wdTov6sW&Fh#;{T6B>-*j()TvPy)QSZ-0WugL=xhiX~vHCjH;oFMianE$}ua582 zpn*Hgk5Q3cL*0shp{{3anYY3e)O*=Tl3WS;@JFZ=??#S~>%$7noZ)5UNz^=>tX?<6 z?@ja^4Z3disJ(g_mC7bm#4V`Q{|S@uJjUYy`Y>#!XD+5vUu5;QsOwsf3hWJQ{{pqZ zME@*r;8fI}I!wc7Q5iaj+LEKFz}w9()VTjfE$nO5 z_rxDG+iMt$qiFD<_NK_{t55^~5cS2{iW=ybsKA<0fxV0BcNVj;6SaVVIo_FwM5R6s z6+kkQaen_P=-N$24X^++aXC)J&6t7*F%VCq2KdOlfI6I)aUq7y^#WRk4^po|ZNX90 zmYqgz@f9r9{qLus55&ZI-v8q*MeXq})Q9L0PSOiF7DwLC?*LA~2e1~i@r2onN_}{_ z7g#FlFjk;4ybZbW?iJL(Kivfi+S7i_!N~bu08=o7dKL0};WlADHk)0TMt$T0?{F1j z7WJJt27iajY&&WzKC$OFQRBvwF8$89sT5N22@Jv)F%0*jQgr~su@y()d#IG2LLIh$ znthl+eF!y9!Xo}bipi+0YeKz$2({27i^#uH@-YpXXaF@}K!w*a4mqeU36eo^Ien3=9aXn7JKcmjV0B*n$i\n" "Language: tr_TR\n" @@ -65,14 +65,18 @@ msgstr "Kalıcı kod" msgid "Invitation codes" msgstr "Davetiye kodları" +#: application/templates/admin.html:37 +msgid "one code per line" +msgstr "Her satırda bir kod" + #: application/templates/admin.html:43 msgid "Accounts" msgstr "Hesaplar" #: application/templates/admin.html:43 #: application/templates/adv_whitelist.html:29 application/templates/my.html:32 -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add" msgstr "Ekle" @@ -482,7 +486,7 @@ msgstr "Kindle ile oku" msgid "Verified" msgstr "Doğrulanmış" -#: application/templates/base.html:59 application/view/login.py:73 +#: application/templates/base.html:59 application/view/login.py:75 #: application/view/share.py:145 msgid "The username does not exist or password is wrong." msgstr "Kullanıcı adı mevcut değil veya şifre yanlış." @@ -579,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:208 +#: application/templates/base.html:83 application/view/admin.py:199 #: application/view/share.py:135 msgid "The username or password is empty." msgstr "Kullanıcı adı veya şifre boş." -#: application/templates/base.html:84 application/view/admin.py:85 -#: application/view/admin.py:172 application/view/admin.py:212 -#: application/view/login.py:207 application/view/login.py:267 +#: 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 msgid "The two new passwords are dismatch." msgstr "İki yeni şifre uyuşmuyor." @@ -598,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:117 +#: application/templates/base.html:87 application/view/login.py:121 msgid "login required" msgstr "Giriş yapılması gerekiyor" @@ -718,8 +722,8 @@ msgstr "" msgid "Search" msgstr "Ara" -#: application/templates/login.html:34 application/view/login.py:184 -#: application/view/login.py:191 +#: application/templates/login.html:34 application/view/login.py:188 +#: application/view/login.py:195 msgid "" "The website does not allow registration. You can ask the owner for an " "account." @@ -972,134 +976,134 @@ msgstr "Davetiye kodu" msgid "User account" msgstr "Kullanıcı hesabı" -#: application/view/admin.py:50 application/view/setting.py:96 +#: application/view/admin.py:51 application/view/setting.py:96 msgid "Settings Saved!" msgstr "Ayarlar Kaydedildi!" -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add account" msgstr "Hesap ekle" -#: application/view/admin.py:67 application/view/admin.py:116 -#: application/view/admin.py:144 +#: application/view/admin.py:68 application/view/admin.py:107 +#: application/view/admin.py:135 msgid "You do not have sufficient privileges." msgstr "Yeterli yetkiniz yok." -#: application/view/admin.py:81 application/view/admin.py:159 -#: application/view/login.py:203 application/view/login.py:232 +#: application/view/admin.py:82 application/view/admin.py:150 +#: application/view/login.py:207 application/view/login.py:236 #: application/view/setting.py:61 application/view/setting.py:63 #: application/view/setting.py:65 application/view/share.py:35 msgid "Some parameters are missing or wrong." msgstr "Bazı parametreler eksik veya yanlış." -#: application/view/admin.py:83 application/view/login.py:42 -#: application/view/login.py:209 +#: application/view/admin.py:84 application/view/login.py:43 +#: application/view/login.py:213 msgid "The username includes unsafe chars." msgstr "Kullanıcı adı güvensiz karakterler içeriyor." -#: application/view/admin.py:87 application/view/login.py:211 +#: application/view/admin.py:88 application/view/login.py:215 msgid "Already exist the username." msgstr "Kullanıcı adı zaten var." -#: application/view/admin.py:93 application/view/admin.py:178 -#: application/view/admin.py:205 application/view/login.py:258 +#: application/view/admin.py:91 application/view/admin.py:169 +#: application/view/admin.py:196 application/view/login.py:262 msgid "The password includes non-ascii chars." msgstr "Şifre ascii olmayan karakterler içeriyor." -#: application/view/admin.py:120 application/view/admin.py:141 -#: application/view/admin.py:170 +#: application/view/admin.py:111 application/view/admin.py:132 +#: application/view/admin.py:161 msgid "The username '{}' does not exist." msgstr "'{}' kullanıcı adı mevcut değil." -#: application/view/admin.py:136 +#: application/view/admin.py:127 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:137 application/view/admin.py:195 +#: application/view/admin.py:128 application/view/admin.py:186 msgid "Change account" msgstr "Hesabı değiştir" -#: application/view/admin.py:138 application/view/admin.py:196 +#: application/view/admin.py:129 application/view/admin.py:187 msgid "Change" msgstr "Değiştir" -#: application/view/admin.py:193 +#: application/view/admin.py:184 msgid "Change success." msgstr "Değişim başarılı." -#: application/view/admin.py:210 +#: application/view/admin.py:201 msgid "The old password is wrong." msgstr "Eski şifre yanlış." -#: application/view/admin.py:217 +#: application/view/admin.py:208 msgid "Change password success." msgstr "Şifre değiştirme başarılı." -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 -#: application/view/adv.py:74 application/view/adv.py:75 #: 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 msgid "Append hyperlink '{}' to article" msgstr "'{}' linkini makaleye ekle" -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 +#: application/view/adv.py:76 application/view/adv.py:77 +#: application/view/adv.py:78 application/view/adv.py:79 msgid "Save to {}" msgstr "{} kaydedildi" -#: application/view/adv.py:70 +#: application/view/adv.py:76 msgid "evernote" msgstr "evernote" -#: application/view/adv.py:71 +#: application/view/adv.py:77 msgid "wiz" msgstr "wiz" -#: application/view/adv.py:72 +#: application/view/adv.py:78 msgid "pocket" msgstr "pocket" -#: application/view/adv.py:73 +#: application/view/adv.py:79 msgid "instapaper" msgstr "instapaper" -#: application/view/adv.py:74 application/view/adv.py:75 -#: application/view/adv.py:76 application/view/adv.py:77 -#: application/view/adv.py:78 +#: 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ş" -#: application/view/adv.py:74 +#: application/view/adv.py:80 msgid "weibo" msgstr "weibo" -#: application/view/adv.py:75 +#: application/view/adv.py:81 msgid "tencent weibo" msgstr "tencent weibo" -#: application/view/adv.py:76 +#: application/view/adv.py:82 msgid "facebook" msgstr "facebook" -#: application/view/adv.py:78 +#: application/view/adv.py:84 msgid "tumblr" msgstr "tumblr" -#: application/view/adv.py:79 +#: application/view/adv.py:85 msgid "Open in browser" msgstr "Tarayıcıda aç" -#: application/view/adv.py:352 +#: application/view/adv.py:359 msgid "Authorization Error!
{}" msgstr "Yetkilendirme Hatası!
{}" -#: application/view/adv.py:375 +#: application/view/adv.py:382 msgid "Success authorized by Pocket!" msgstr "Pocket tarafından yetkilendirilen başarı!" -#: application/view/adv.py:381 +#: application/view/adv.py:388 msgid "" "Failed to request authorization of Pocket!
See details " "below:

{}" @@ -1107,26 +1111,26 @@ msgstr "" "Pocket yetkilendirme isteği başarısız oldu!
Aşağıdaki ayrıntılara " "bakın:

{}" -#: application/view/adv.py:391 +#: application/view/adv.py:398 msgid "Request type [{}] unsupported" msgstr "İstek türü [{}] desteklenmiyor" -#: application/view/adv.py:406 +#: application/view/adv.py:413 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:68 application/view/login.py:154 +#: application/view/deliver.py:67 application/view/login.py:158 #: application/view/share.py:39 msgid "The username does not exist or the email is empty." msgstr "Kullanıcı adı mevcut değil veya e-posta boş." -#: application/view/deliver.py:84 +#: application/view/deliver.py:83 msgid "The following recipes has been added to the push queue." msgstr "Aşağıdaki tarifler itme kuyruğuna eklendi." -#: application/view/deliver.py:87 +#: application/view/deliver.py:86 msgid "There are no recipes to deliver." msgstr "Teslim edilecek tarif yok." @@ -1134,83 +1138,83 @@ 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:192 -#: application/view/subscribe.py:304 application/view/subscribe.py:333 -#: application/view/subscribe.py:341 +#: 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 msgid "The recipe does not exist." msgstr "Tarif mevcut değil." -#: application/view/login.py:25 +#: application/view/login.py:26 msgid "Please use {}/{} to login at first time." msgstr "İlk giriş için kullanıcı adı:'{}' ve şifre: '{}'" -#: application/view/login.py:38 +#: application/view/login.py:39 msgid "Username is empty." msgstr "Kullanıcı adı boş." -#: application/view/login.py:40 +#: application/view/login.py:41 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:74 +#: application/view/login.py:76 msgid "Forgot password?" msgstr "Forgot password?" -#: application/view/login.py:133 application/view/login.py:269 +#: application/view/login.py:137 application/view/login.py:273 msgid "The token is wrong or expired." msgstr "Belirteç yanlış veya süresi dolmuş." -#: application/view/login.py:136 +#: application/view/login.py:140 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:138 +#: application/view/login.py:142 msgid "The email of account '{name}' is {email}." msgstr "'{name}' hesabının e-postası {email}." -#: application/view/login.py:159 +#: application/view/login.py:163 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:162 +#: application/view/login.py:166 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:173 +#: application/view/login.py:177 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:174 +#: application/view/login.py:178 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:205 +#: application/view/login.py:209 msgid "The invitation code is invalid." msgstr "Davetiye kodu geçersiz." -#: application/view/login.py:213 +#: application/view/login.py:217 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:223 +#: application/view/login.py:227 msgid "Successfully created account." msgstr "Hesap başarıyla oluşturuldu." -#: application/view/login.py:234 +#: application/view/login.py:238 msgid "Reset KindleEar password" msgstr "KindleEar şifrenizi sıfırlama" -#: application/view/login.py:235 +#: application/view/login.py:239 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:236 +#: application/view/login.py:240 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 " @@ -1328,7 +1332,7 @@ msgstr "Tagalog" msgid "Hausa" msgstr "Hausa" -#: application/view/share.py:50 application/view/subscribe.py:240 +#: application/view/share.py:50 application/view/subscribe.py:239 msgid "Unknown command: {}" msgstr "Bilinmeyen komut: {}" @@ -1383,27 +1387,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:265 +#: application/view/subscribe.py:123 application/view/subscribe.py:264 msgid "Failed to save the recipe. Error:" msgstr "Tarif kaydedilemedi. Hata:" -#: application/view/subscribe.py:221 +#: application/view/subscribe.py:220 msgid "You can only delete the uploaded recipe." msgstr "Yalnızca yüklenen tarifi silebilirsiniz." -#: application/view/subscribe.py:225 +#: application/view/subscribe.py:224 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:238 +#: application/view/subscribe.py:237 msgid "This recipe has not been subscribed to yet." msgstr "Bu tarife henüz abone olunmadı." -#: application/view/subscribe.py:252 +#: application/view/subscribe.py:251 msgid "Can not read uploaded file, Error:" msgstr "Yüklenen dosya okunamıyor, Hata:" -#: application/view/subscribe.py:260 +#: application/view/subscribe.py:259 msgid "" "Failed to decode the recipe. Please ensure that your recipe is saved in " "utf-8 encoding." @@ -1411,15 +1415,15 @@ msgstr "" "Tarif çözümlenemedi. Lütfen tarifinizin utf-8 kodlamasında " "kaydedildiğinden emin olun." -#: application/view/subscribe.py:280 +#: application/view/subscribe.py:279 msgid "The recipe is already in the library." msgstr "Tarif zaten kütüphanede." -#: application/view/subscribe.py:311 +#: application/view/subscribe.py:309 msgid "The login information for this recipe has been cleared." msgstr "Bu tarifin giriş bilgileri temizlendi." -#: application/view/subscribe.py:315 +#: application/view/subscribe.py:313 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 949bd9ceab9d92775f7104f7e53775bf5942f178..82980cc6d568b91fcf85237faaf56eebe1354e11 100644 GIT binary patch delta 5183 zcmYM$3s6_b702-f2_Pa0_&`MXBOnTje<7$9vI(`bZ0t^h(q5 zQH_&`Iti_{YSmN&wWCv$u_m!KF_kh&jI}|mNv){K5Z@ExgK59N+%r?h;j?G&-m_=- z?!BskQ zkOBVqNV6a6`XE2{TnY^(9)UwK#|D&RFXBq->b@6Hg>AzC?#669 zZv8h=^W4Eg3?1m)80R@xLSr}`wKxnnVqe^kO8gdTQ_f%%Ua;#|t-lAe=#LoW7nF-( z#097ci&6KNp%(HeYQAa=XMWdU9cwU!cpqk9D+cj8rsMFz>=KruR=xza;-6qY{29jJ z28*|10&%m&@1Y7hjmdZpJ*~8dhPJd1qm>{QwbcPs;&B$2;5REUt5G|$5J}-ypz^$k zDy$JTVH4`U9jG(bf||EAf&EvboeoWW$p(I9h7R#p5QAEACXT|O^?w&N;WD!hV~IUf z-j}fwx8W|li7M#TM8ELuiR^zI9k0_-fp6n1?8}4Kv#;TY`WrA6KR``*9koN z2GkC|f_h|oPz4@Bt@wRZVIP|9s2%do(_mNJHPpmWoD2<&MGm%0L?sBKRx%Mat`N0? z>By_@%2B7h8nqM8pcb&$tiw>^RjB-H{eI7Fq@l{Up(<)Y74&D+N=~B^UqnrO4V9o9 z`Txx;qx(~MbyR2$@)X^8%)v)c`Bq{fHsMHY#|pjwp#gs-52H?V6>6fnsDukFu0?I> zv#0`AqqhDf)JpfDcI+VP5S~Dtt+N(iMK;-m@H{!+E(K$l-%X@Zgr%rTSEDB0j@psm zqqg)AYReC!cH|wqegakaC#d`XgIYijs(>)QHo86-b!LX63d})I1Bz*A!b;T2=AgEI zKI)k-N3FabId^UyD$#4Gi4LM(%fl9TqVDfPjk|{`JTlGyu=PdFmzT!=D?tGrs&FbQ zu0XBuG1N1zw(HMhHt`zNRvt7@VlU!rsKmF-d#Fbg#;Y?0hobV&#%VY=o&DFBYZo0l zG)GVqeu8D#japf829F6BqT-#X!_|fp@E)e%{Ue;q#m8_mu0~xyhWU68HBWA)|0%EV zXcW+~9CcwoPRCXxr;FvN@)+F&RAIA`MY*3^|F3Zz@mr{gui5o9s$|z)DbB=&sI&1V zs?d)yfZk0SdR7Cn`~m5xEiT0IxX|M5*o(Lg!|)^2vptQy@jTv#T^NNoP;W&K>aazT zRF5JFLop8*F~1v6Ly31_1nx$?9xbR3%W>?39p)trC%%b!cn1TRGul7x4`Dg+Jk-R^ zsFlBsdYjr&3%G>QdjGpMA zuMN&L2c;~)E560W3UZX@L97HHSra@ zejBy&$e_P~MC?nPYVlZ9-tp+EvdJ{ohH%PlzzI&2R`d~StJ+aJan9mSi@!jPyKeDq zQzud5BT(04PzxAj{Q=aMGBcO`kE1cgErn+AL^jzSw*D@Uh7Q93z7AP9#e4=eun~3RF|!p_;7N?bPc6QI1Bw5O zns^|~)_obM@5X4<_y?@N8kNVZvBpxn;GtH&%KBe2x0-uV6CFa0YeP-+PxBlqaVPS} zk-LE!7sF>z{Rw7aF<_mH4{#-?cb& zlHVU~4no}*Ff;K1;vB`y@9Jr2B@L*FH=$1J0c^%kQH8w7FABZKO_+eMp$a&PDzFtb zt^+mUWxIaW`fsCteTVa#N!QcRQzMIpp6NJL!crUXi1`F6@sp@=i%}E)9JLc0?fMqf z{X4C{+2TK%hf#UnMJ@Q`WcELiMyGY?m$V9sHxtcN)WjoE6BnW;nu$vE1oH88i%{d5 zFdz4$-lB^*69*Ui`JYA=^20**UlZ0_M}rO6fSPDCYNB1J#DB2(UDU)Mpc0)&J(8hLW>jo;zXPyvTg3IBrH(tlWgJL<;I%rDJ5s525#?BAb<$;6XT z<7b;sqw+38O}Gj*-!{|RO+y!&t>ZmZf;JqAXDt58>{a6T_eUifjT)banyA>~N>o8} zE%s33*Q0i{(T_d1)h_%FwN?9Uz){pExfRp#id~QAcc^w~6zUO;K@~6sb$>bX-_7NR zCftcS8+%a&wxh;%VY1%;&~H1(tLsuR4d>u6^iVsn6?KT-ME#Qa0+lFXn*S$THs%x0 zK#hA2HLe*ot^<|t3i4~kMe~tS0mWFv{O)lYskp^_0|UfoP><#=s*nLQ{GAA58gUis zzB<(PUm-)?A&YxZk6=it??hBVvv3G5L{ABRK|`lCaafseteK2@ zmf05Pqwb%IgR$DKKZ_3$uSDhj5S8y#8T+pa|4m0M>dzqkB@>65c$k@v1;p8?2^OGM z@&fW|yDg}*a0>N}zkvEuevO(ayxh+hkG+W#QTL~pv;XRtU;|2QKp86Gqc(6Js`3@6 zgmrd(mAM65=--VRSH%aOH`L9>EPNGJ_%Y-dxX*A6W_S5XjA)6g;ptxGX99!W%0uZ(q;POTTSsy!&AAf83xN6951J delta 5121 zcmYM$3s9C-9>?*6uYg( zgHk!-s%sas(s3Q~vg$70s5Tl#)V9XWSXYz5q2>tG9T^?6!Z<9WVj^nb(-@1-qZY15-GTjBho@0Tx{w>o z^>Edw6Rbr&vW=(&x1)A^0F~JLW-IE1LMJJ3Dy{>y@DJ7@j2ps*cG0K_0;rvgMfID2 z+CdKTO1nJNEiXfz#C+5Seqla>eTaXDO5{neKIERIpv>2xGTMwv=pEEfT2K>zhFZ7- zHNhq1zq`s0jgRNmQKEk2>A4_Iz`IcMJ%I(d8pmQQmg@cQrl6fn4|tbi7HXlns0qt0 zu0$Q_uTTjrLmmAysGV*^o!C~?B|L<>TPH03964Oqg}S`)Jl8nZcVj6OVlFDvWvGSM zqE2KJ>PWYv5^X@8$R2AygnA8+qQ?InwSg>4S z5-OgC+F=p$cwL#bKZY5^HK?Q9YJPw|;ttfr7tK!8Bf5suFecrbzZi>%=caT1`f{zO zLYHPIYQdv88!w@DmPy)oUyId^83e?@$ zfl9O)0~oqMLC>l$zrxgEAnJ%G;1sN|crE&fo3Jnb74>XeFbYp%IG)95Jdb)SuAna4 z52!~G%jA794IgEF7o?zx>oF2Hpk9y7s1Hjc#^7gH)yVVqm@lRNf*HO><<%!;r?nE8&K8(XARKh3BQ>ca8 zt^I4%3H4xq?6tUmrZ?X}yp<4!R)w(`1r7KkH%U9$jXI)6RQ(Z)n=L+y>i4n5ZDzaq zg|&C062E5kH&I{6aBiwjI(3r0{}ZT)r6LE_VTQ%?&4s8tu^5$59ge|zR62$_?Ec?mGK@-#3qYB!DQlpp%(rR zYMhVl>kdVu`rn4C&qvKO(+thEh6SjdS6Rd3=5n(Zwa|K0zXsHVe>RVxCT>Rl2y&mG z`gL0Ucczceq535t<3nyZ1!bP@Rk+Eh3GT!MEJscBFzN%d7`3AsvkvDGZ^k#V8tC)n_kR04W)ED&OjYmEh@w9sKoZ8Cj1cf+8jqs{IS)awz$K* zY+keWn`U?(-wMXXqUM{AiLCD`DQMxvsN1?0cVZJNkw-~a?{N(d!52^oyn#w^2ddve z)Wj{;{*l$Up?+;&wDvH5GpS!c4C$H1Q_zH?tzn`$1vT+BRKF6`f)Aih;t6YCiWgz0CV{Sst^ET?y?ZFgm&gcA9(N2Xj>N0!GUev-7{PNbq!%z#2M~$0;d@Nlts$UJ} z;LE7D=tC^SZ&C9X6?lovLB;b6IDchUX%)XgEwlu+&~vDXU$XdZ)WSPaQA}oBu%FkuOja44CfSogt|H zxn>b+-k+ittU~pF%3O)657k-4R@4Lyn2HB1{?t5Y_5Vgq6n&T1e*kJBzr|VTCoZsf z0jmEZ)QMJGyxfz&|0(FGR#}HPP@m)-$X{RXgtd2}PAHP!p?X9ysDx8d(F| z;0n~;s6{2%i0XF?m0$->)%)K|A&rWuGrd157objHIqDK^ME#OEikj#<)a{Kb@-ACC zs$T`FUmdF7K^%@JFdHwU67bLR{@kC8BU#@qRRK3(01u#^&1qC3-I$JXcYBFuqxzSl z+Mh(axb+sFLGAE+bI?6rLOD2;`q`-Q4`E2RawP>#bQpcuh7ouMb;Re*Z_FF0XBjoy zYfnOr58z(4T za%-H{VJbw2s)}rqx-U+<;0EIXz8ZZVAnJw6l_@sFT zqlqtKEPjJZ{D#H8QZG(02c!D=_q8S!4qKO!xjHf{H;^?sZ_>Vo%q7FtWt2|*AI{Sf AqW}N^ diff --git a/application/translations/zh/LC_MESSAGES/messages.po b/application/translations/zh/LC_MESSAGES/messages.po index 7fc6e0cb..4c172fee 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-17 14:29-0300\n" +"POT-Creation-Date: 2024-02-27 22:09-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh\n" @@ -65,14 +65,18 @@ msgstr "永久邀请码" msgid "Invitation codes" msgstr "邀请码列表" +#: application/templates/admin.html:37 +msgid "one code per line" +msgstr "一行一码" + #: application/templates/admin.html:43 msgid "Accounts" msgstr "用户账号列表" #: application/templates/admin.html:43 #: application/templates/adv_whitelist.html:29 application/templates/my.html:32 -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add" msgstr "添加" @@ -474,7 +478,7 @@ msgstr "在Kindle阅读" msgid "Verified" msgstr "已校验" -#: application/templates/base.html:59 application/view/login.py:73 +#: application/templates/base.html:59 application/view/login.py:75 #: application/view/share.py:145 msgid "The username does not exist or password is wrong." msgstr "用户名不存在或密码错误。" @@ -571,14 +575,14 @@ msgstr "这个Recipe的自定义推送时间已经设定成功。" msgid "The account have been deleted." msgstr "这个账号已经被删除。" -#: application/templates/base.html:83 application/view/admin.py:208 +#: application/templates/base.html:83 application/view/admin.py:199 #: application/view/share.py:135 msgid "The username or password is empty." msgstr "用户名或密码为空。" -#: application/templates/base.html:84 application/view/admin.py:85 -#: application/view/admin.py:172 application/view/admin.py:212 -#: application/view/login.py:207 application/view/login.py:267 +#: 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 msgid "The two new passwords are dismatch." msgstr "两个密码不匹配。" @@ -590,7 +594,7 @@ msgstr "修改密码成功。" msgid "Account added successfully." msgstr "添加账号成功。" -#: application/templates/base.html:87 application/view/login.py:117 +#: application/templates/base.html:87 application/view/login.py:121 msgid "login required" msgstr "需要登录" @@ -702,8 +706,8 @@ msgstr "使用开源 %(kindleear)s 应用,您可以部署您自己的网站, msgid "Search" msgstr "搜索" -#: application/templates/login.html:34 application/view/login.py:184 -#: application/view/login.py:191 +#: application/templates/login.html:34 application/view/login.py:188 +#: application/view/login.py:195 msgid "" "The website does not allow registration. You can ask the owner for an " "account." @@ -951,157 +955,157 @@ msgstr "邀请码" msgid "User account" msgstr "账号" -#: application/view/admin.py:50 application/view/setting.py:96 +#: application/view/admin.py:51 application/view/setting.py:96 msgid "Settings Saved!" msgstr "恭喜,保存成功!" -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add account" msgstr "添加账号" -#: application/view/admin.py:67 application/view/admin.py:116 -#: application/view/admin.py:144 +#: application/view/admin.py:68 application/view/admin.py:107 +#: application/view/admin.py:135 msgid "You do not have sufficient privileges." msgstr "您没有足够的权限。" -#: application/view/admin.py:81 application/view/admin.py:159 -#: application/view/login.py:203 application/view/login.py:232 +#: application/view/admin.py:82 application/view/admin.py:150 +#: application/view/login.py:207 application/view/login.py:236 #: application/view/setting.py:61 application/view/setting.py:63 #: application/view/setting.py:65 application/view/share.py:35 msgid "Some parameters are missing or wrong." msgstr "一些参数为空或错误。" -#: application/view/admin.py:83 application/view/login.py:42 -#: application/view/login.py:209 +#: application/view/admin.py:84 application/view/login.py:43 +#: application/view/login.py:213 msgid "The username includes unsafe chars." msgstr "用户名包含不安全字符。" -#: application/view/admin.py:87 application/view/login.py:211 +#: application/view/admin.py:88 application/view/login.py:215 msgid "Already exist the username." msgstr "此账号已经存在。" -#: application/view/admin.py:93 application/view/admin.py:178 -#: application/view/admin.py:205 application/view/login.py:258 +#: application/view/admin.py:91 application/view/admin.py:169 +#: application/view/admin.py:196 application/view/login.py:262 msgid "The password includes non-ascii chars." msgstr "密码包含非ASCII字符。" -#: application/view/admin.py:120 application/view/admin.py:141 -#: application/view/admin.py:170 +#: application/view/admin.py:111 application/view/admin.py:132 +#: application/view/admin.py:161 msgid "The username '{}' does not exist." msgstr "账号名 '{}' 不存在。" -#: application/view/admin.py:136 +#: application/view/admin.py:127 msgid "The password will not be changed if the fields are empties." msgstr "如果不填写密码,则密码不会被修改。" -#: application/view/admin.py:137 application/view/admin.py:195 +#: application/view/admin.py:128 application/view/admin.py:186 msgid "Change account" msgstr "修改用户账号" -#: application/view/admin.py:138 application/view/admin.py:196 +#: application/view/admin.py:129 application/view/admin.py:187 msgid "Change" msgstr "修改" -#: application/view/admin.py:193 +#: application/view/admin.py:184 msgid "Change success." msgstr "修改成功。" -#: application/view/admin.py:210 +#: application/view/admin.py:201 msgid "The old password is wrong." msgstr "原密码错误。" -#: application/view/admin.py:217 +#: application/view/admin.py:208 msgid "Change password success." msgstr "修改密码成功。" -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 -#: application/view/adv.py:74 application/view/adv.py:75 #: 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 msgid "Append hyperlink '{}' to article" msgstr "在每篇文章后附加 '{}' 超链接" -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 +#: application/view/adv.py:76 application/view/adv.py:77 +#: application/view/adv.py:78 application/view/adv.py:79 msgid "Save to {}" msgstr "保存到 {}" -#: application/view/adv.py:70 +#: application/view/adv.py:76 msgid "evernote" msgstr "evernote" -#: application/view/adv.py:71 +#: application/view/adv.py:77 msgid "wiz" msgstr "为知笔记" -#: application/view/adv.py:72 +#: application/view/adv.py:78 msgid "pocket" msgstr "pocket" -#: application/view/adv.py:73 +#: application/view/adv.py:79 msgid "instapaper" msgstr "instapaper" -#: application/view/adv.py:74 application/view/adv.py:75 -#: application/view/adv.py:76 application/view/adv.py:77 -#: application/view/adv.py:78 +#: 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 "分享到 {}" -#: application/view/adv.py:74 +#: application/view/adv.py:80 msgid "weibo" msgstr "微博" -#: application/view/adv.py:75 +#: application/view/adv.py:81 msgid "tencent weibo" msgstr "腾讯微博" -#: application/view/adv.py:76 +#: application/view/adv.py:82 msgid "facebook" msgstr "facebook" -#: application/view/adv.py:78 +#: application/view/adv.py:84 msgid "tumblr" msgstr "tumblr" -#: application/view/adv.py:79 +#: application/view/adv.py:85 msgid "Open in browser" msgstr "在浏览器打开" -#: application/view/adv.py:352 +#: application/view/adv.py:359 msgid "Authorization Error!
{}" msgstr "申请授权过程失败!
{}" -#: application/view/adv.py:375 +#: application/view/adv.py:382 msgid "Success authorized by Pocket!" msgstr "已经成功获得Pocket的授权!" -#: application/view/adv.py:381 +#: application/view/adv.py:388 msgid "" "Failed to request authorization of Pocket!
See details " "below:

{}" msgstr "申请Pocket授权失败!
错误信息参考如下:

{}" -#: application/view/adv.py:391 +#: application/view/adv.py:398 msgid "Request type [{}] unsupported" msgstr "不支持你请求的命令类型 [{}]" -#: application/view/adv.py:406 +#: application/view/adv.py:413 msgid "The Instapaper service encountered an error. Please try again later." msgstr "Instapaper服务器异常,请稍候再试。" -#: application/view/deliver.py:68 application/view/login.py:154 +#: application/view/deliver.py:67 application/view/login.py:158 #: application/view/share.py:39 msgid "The username does not exist or the email is empty." msgstr "用户名不存在或email为空。" -#: application/view/deliver.py:84 +#: application/view/deliver.py:83 msgid "The following recipes has been added to the push queue." msgstr "下列Recipe已经被添加到推送队列。" -#: application/view/deliver.py:87 +#: application/view/deliver.py:86 msgid "There are no recipes to deliver." msgstr "没有需要推送的Recipe。" @@ -1109,79 +1113,79 @@ msgstr "没有需要推送的Recipe。" msgid "Cannot fetch data from {}, status: {}" msgstr "无法从 {} 获取数据,状态: {}" -#: application/view/library.py:48 application/view/subscribe.py:192 -#: application/view/subscribe.py:304 application/view/subscribe.py:333 -#: application/view/subscribe.py:341 +#: 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 msgid "The recipe does not exist." msgstr "此Recipe不存在。" -#: application/view/login.py:25 +#: application/view/login.py:26 msgid "Please use {}/{} to login at first time." msgstr "初次登录请使用用户名'{}'/密码'{}'。" -#: application/view/login.py:38 +#: application/view/login.py:39 msgid "Username is empty." msgstr "账号名为空。" -#: application/view/login.py:40 +#: application/view/login.py:41 msgid "The len of username reached the limit of 25 chars." msgstr "用户名超过25字符。" -#: application/view/login.py:74 +#: application/view/login.py:76 msgid "Forgot password?" msgstr "忘记密码?" -#: application/view/login.py:133 application/view/login.py:269 +#: application/view/login.py:137 application/view/login.py:273 msgid "The token is wrong or expired." msgstr "Token码错误或已经逾期。" -#: application/view/login.py:136 +#: application/view/login.py:140 msgid "Please input the correct username and email to reset password." msgstr "请输入正确的用户名和Email来重置密码。" -#: application/view/login.py:138 +#: application/view/login.py:142 msgid "The email of account '{name}' is {email}." msgstr "账号 '{name}' 的Email为 {email}" -#: application/view/login.py:159 +#: application/view/login.py:163 msgid "Reset password success, Please close this page and login again." msgstr "修改密码成功,请关闭此页面重新登录。" -#: application/view/login.py:162 +#: application/view/login.py:166 msgid "The email you input is not associated with this account." msgstr "您输入的email不正确。" -#: application/view/login.py:173 +#: application/view/login.py:177 msgid "The link to reset your password has been sent to your email." msgstr "重置密码的邮件已经被发送至你的email。" -#: application/view/login.py:174 +#: application/view/login.py:178 msgid "Please check your email inbox within 24 hours." msgstr "请在24小时内检查你的email收件箱。" -#: application/view/login.py:205 +#: application/view/login.py:209 msgid "The invitation code is invalid." msgstr "邀请码无效。" -#: application/view/login.py:213 +#: application/view/login.py:217 msgid "" "Failed to create an account. Please contact the administrator for " "assistance." msgstr "创建账号失败,请联系管理员请求协助。" -#: application/view/login.py:223 +#: application/view/login.py:227 msgid "Successfully created account." msgstr "成功创建账号。" -#: application/view/login.py:234 +#: application/view/login.py:238 msgid "Reset KindleEar password" msgstr "重置KindleEar密码" -#: application/view/login.py:235 +#: application/view/login.py:239 msgid "This is an automated email. Please do not reply to it." msgstr "这个是自动发送的邮件,请勿直接回复。" -#: application/view/login.py:236 +#: application/view/login.py:240 msgid "You can click the following link to reset your KindleEar password." msgstr "你可以点击下面的链接来重置你的KindleEar密码。" @@ -1297,7 +1301,7 @@ msgstr "他加禄语" msgid "Hausa" msgstr "豪萨语" -#: application/view/share.py:50 application/view/subscribe.py:240 +#: application/view/share.py:50 application/view/subscribe.py:239 msgid "Unknown command: {}" msgstr "未知命令:{}" @@ -1352,41 +1356,41 @@ msgstr "标题或URL为空。" msgid "Failed to fetch the recipe." msgstr "抓取Recipe失败。" -#: application/view/subscribe.py:123 application/view/subscribe.py:265 +#: application/view/subscribe.py:123 application/view/subscribe.py:264 msgid "Failed to save the recipe. Error:" msgstr "保存Recipe失败。错误:" -#: application/view/subscribe.py:221 +#: application/view/subscribe.py:220 msgid "You can only delete the uploaded recipe." msgstr "您只能删除你自己上传的Recipe。" -#: application/view/subscribe.py:225 +#: application/view/subscribe.py:224 msgid "The recipe have been subscribed, please unsubscribe it before delete." msgstr "此Recipe已经被订阅,请先取消订阅然后再删除。" -#: application/view/subscribe.py:238 +#: application/view/subscribe.py:237 msgid "This recipe has not been subscribed to yet." msgstr "此Recipe尚未被订阅。" -#: application/view/subscribe.py:252 +#: application/view/subscribe.py:251 msgid "Can not read uploaded file, Error:" msgstr "无法读取上传的文件,错误:" -#: application/view/subscribe.py:260 +#: application/view/subscribe.py:259 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:280 +#: application/view/subscribe.py:279 msgid "The recipe is already in the library." msgstr "此Recipe已经在新闻源中。" -#: application/view/subscribe.py:311 +#: application/view/subscribe.py:309 msgid "The login information for this recipe has been cleared." msgstr "此Recipe的网站登录信息已经被删除。" -#: application/view/subscribe.py:315 +#: application/view/subscribe.py:313 msgid "The login information for this recipe has been saved." msgstr "此Recipe的网站登录信息已经保存。" diff --git a/application/utils.py b/application/utils.py index db490ff9..82a81058 100644 --- a/application/utils.py +++ b/application/utils.py @@ -2,7 +2,7 @@ # -*- coding:utf-8 -*- #一些常用工具函数 -import os, sys, hashlib, base64, random, datetime +import os, sys, hashlib, base64, secrets, datetime from urllib.parse import urlparse #当异常出现时,使用此函数返回真实引发异常的文件名,函数名和行号 @@ -82,17 +82,17 @@ def hide_website(site): return site #-----------以下几个函数为安全相关的 -def new_secret_key(length=8): - allchars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789' - return ''.join([random.choice(allchars) for i in range(length)]) +def new_secret_key(length=12): + allchars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXZYabcdefghijklmnopqrstuvwxyz' + return ''.join([secrets.choice(allchars) for i in range(length)]) def ke_encrypt(txt: str, key: str): - return _ke_auth_code(txt, key, 'ENCODE') + return _ke_auth_code(txt, key, 'encode') def ke_decrypt(txt: str, key: str): - return _ke_auth_code(txt, key, 'DECODE') + return _ke_auth_code(txt, key, 'decode') -def _ke_auth_code(txt: str, key: str, operation: str='DECODE'): +def _ke_auth_code(txt: str, key: str, act: str='decode'): if not txt: return '' @@ -103,7 +103,7 @@ def _ke_auth_code(txt: str, key: str, operation: str='DECODE'): cryptKey = keyA + hashlib.md5(keyA.encode('utf-8')).hexdigest() keyLength = len(cryptKey) - if operation == 'DECODE': + if act == 'decode': try: txt = base64.urlsafe_b64decode(txt).decode('utf-8') except: @@ -133,7 +133,7 @@ def _ke_auth_code(txt: str, key: str, operation: str='DECODE'): box[j] = tmp result += chr(ord(txt[i]) ^ (box[(box[a] + box[j]) % 256])) - if operation == 'DECODE': + if act == 'decode': if result[:16] == hashlib.md5((result[16:] + keyB).encode('utf-8')).hexdigest()[:16]: return result[16:] else: diff --git a/application/view/admin.py b/application/view/admin.py index 9f925303..fbca8228 100644 --- a/application/view/admin.py +++ b/application/view/admin.py @@ -9,6 +9,7 @@ from ..base_handler import * from ..back_end.db_models import * from ..utils import new_secret_key, str_to_int +from .login import CreateAccountIfNotExist bpAdmin = Blueprint('bpAdmin', __name__) @@ -85,20 +86,10 @@ def AdminAddAccountPost(): tips = _("The two new passwords are dismatch.") elif KeUser.get_or_none(KeUser.name == username): tips = _("Already exist the username.") - else: - secret_key = new_secret_key() - try: - pwd = hashlib.md5((password1 + secret_key).encode()).hexdigest() - except: - tips = _("The password includes non-ascii chars.") - else: - send_mail_service = {'service': 'admin'} if sm_service == 'admin' else {} - user = KeUser(name=username, passwd=pwd, timezone=app.config['TIMEZONE'], secret_key=secret_key, - expiration_days=expiration, share_links={'key': new_secret_key()}, - email=email, send_mail_service=send_mail_service) - if expiration: - user.expires = datetime.datetime.utcnow() + datetime.timedelta(days=expiration) - user.save() + elif not CreateAccountIfNotExist(username, password1, email, + {'service': 'admin'} if sm_service == 'admin' else {}, expiration): + tips = _("The password includes non-ascii chars.") + if tips: return render_template('user_account.html', tips=tips, formTitle=_('Add account'), submitTitle=_('Add'), user=None, tab='admin') @@ -119,7 +110,7 @@ def AdminDeleteAccountAjax(): if not u: return {'status': _("The username '{}' does not exist.").format(name)} else: - u.erase_traces() #删除账号订阅的书,白名单,过滤器等,就是完全的清理 + u.erase_traces() #删除账号订阅的书,白名单,过滤器等,完全的清理其痕迹 u.delete_instance() return {'status': 'ok'} diff --git a/application/view/adv.py b/application/view/adv.py index ffb71d22..6dbb921c 100644 --- a/application/view/adv.py +++ b/application/view/adv.py @@ -29,22 +29,28 @@ def AdvDeliverNow(): @bpAdv.route("/adv/whitelist", endpoint='AdvWhiteList') @login_required() def AdvWhiteList(): - user = get_login_user() - return render_template('adv_whitelist.html', tab='advset',user=user, - advCurr='whitelist', adminName=app.config['ADMIN_NAME'], in_email_service=app.config['INBOUND_EMAIL_SERVICE']) + if app.config['INBOUND_EMAIL_SERVICE'] == 'gae': + user = get_login_user() + return render_template('adv_whitelist.html', tab='advset',user=user, + advCurr='whitelist', adminName=app.config['ADMIN_NAME'], in_email_service=app.config['INBOUND_EMAIL_SERVICE']) + else: + abort(404) @bpAdv.post("/adv/whitelist", endpoint='AdvWhiteListPost') @login_required() def AdvWhiteListPost(): - user = get_login_user() - wlist = request.form.get('wlist') - if wlist: - wlist = wlist.replace('"', "").replace("'", "").strip() - if wlist.startswith('*@'): #输入*@xx.xx则修改为@xx.xx - wlist = wlist[1:] + if app.config['INBOUND_EMAIL_SERVICE'] == 'gae': + user = get_login_user() + wlist = request.form.get('wlist') if wlist: - WhiteList.get_or_create(mail=wlist, user=user.name) - return redirect(url_for('bpAdv.AdvWhiteList')) + wlist = wlist.replace('"', "").replace("'", "").strip() + if wlist.startswith('*@'): #输入*@xx.xx则修改为@xx.xx + wlist = wlist[1:] + if wlist: + WhiteList.get_or_create(mail=wlist, user=user.name) + return redirect(url_for('bpAdv.AdvWhiteList')) + else: + abort(404) #删除白名单项目 @bpAdv.route("/advdel", endpoint='AdvDel') diff --git a/application/view/login.py b/application/view/login.py index 41f6fa02..0f55798d 100644 --- a/application/view/login.py +++ b/application/view/login.py @@ -21,8 +21,9 @@ def Login(): # 第一次登陆时如果没有管理员帐号, # 则增加一个管理员帐号 ADMIN_NAME,密码 ADMIN_NAME,后续可以修改密码 tips = '' - if CreateAccountIfNotExist(app.config['ADMIN_NAME']): - tips = (_("Please use {}/{} to login at first time.").format(app.config['ADMIN_NAME'], app.config['ADMIN_NAME'])) + adminName = app.config['ADMIN_NAME'] + if CreateAccountIfNotExist(adminName): + tips = (_("Please use {}/{} to login at first time.").format(adminName, adminName)) session['login'] = 0 session['userName'] = '' @@ -44,7 +45,8 @@ def LoginPost(): if tips: return render_template('login.html', tips=tips) - CreateAccountIfNotExist(app.config['ADMIN_NAME']) #确认管理员账号是否存在 + adminName = app.config['ADMIN_NAME'] + CreateAccountIfNotExist(adminName) #确认管理员账号是否存在 u = KeUser.get_or_none(KeUser.name == name) if u: @@ -56,7 +58,7 @@ def LoginPost(): if u: session['login'] = 1 session['userName'] = name - session['role'] = 'admin' if name == app.config['ADMIN_NAME'] else 'user' + session['role'] = 'admin' if name == adminName else 'user' if u.expires and u.expiration_days > 0: #用户登陆后自动续期 u.expires = datetime.datetime.utcnow() + datetime.timedelta(days=u.expiration_days) u.save() @@ -80,28 +82,30 @@ def LoginPost(): #判断账号是否存在 #如果账号不存在,创建一个,并返回True,否则返回False -def CreateAccountIfNotExist(name, password=None, email=None): +def CreateAccountIfNotExist(name, password=None, email=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() + 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))) return False - send_mail_service = {} - if name != app.config['ADMIN_NAME'] and AppInfo.get_value(AppInfo.newUserMailService, 'admin') == 'admin': - send_mail_service = {'service': 'admin'} + if sm_service is None: + sm_service = {} + if name != app.config['ADMIN_NAME'] and AppInfo.get_value(AppInfo.newUserMailService, 'admin') == 'admin': + sm_service = {'service': 'admin'} - KeUser.create(name=name, passwd=password, kindle_email='', enable_send=False, send_time=6, - timezone=app.config['TIMEZONE'], book_type="epub", device='kindle', expires=None, secret_key=secretKey, - expiration_days=0, share_links={'key': shareKey}, book_title='KindleEar', book_language='en', - email=email, send_mail_service=send_mail_service) + 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 expiration: + user.expires = datetime.datetime.utcnow() + datetime.timedelta(days=expiration) + user.save() return True @bpLogin.route("/logout", methods=['GET', 'POST']) diff --git a/config.py b/config.py index 5d45cf74..838f0ba8 100644 --- a/config.py +++ b/config.py @@ -7,6 +7,7 @@ KindleEar配置文件,请务必修改开始几个配置 如果有的配置是从环境变量获取,也可以使用os.envrion['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 @@ -15,24 +16,23 @@ #Find it at Upper right corner of SERVER_LOCATION = "us-central1" -#Choose the database engine, you can also set Database URL to DATABASE_NAME -#Supports: "datastore", "sqlite", "mysql", "postgresql", "cockroachdb", "mongodb", "redis" -DATABASE_ENGINE = "redis" -DATABASE_NAME = "test.db" # or "mongodb://localhost:27017/", for redis it is db number -DATABASE_HOST = "127.0.0.1" -DATABASE_PORT = 6379 -DATABASE_USERNAME = "" -DATABASE_PASSWORD = "" +#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' -#Email receiving service, "gae" | "" +#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 celery | rq +#If task queue service is apscheduler, celery, rq +#Options: 'redis://', 'mongodb://', 'sqlite://', 'mysql://', 'postgresql://' +#For apscheduler, it can be a empty str '' if a memory store is used +#For rq, only 'redis://' is supported TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/" -TASK_QUEUE_RESULT_BACKEND = "redis://127.0.0.1:6379/" #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 @@ -55,3 +55,6 @@ #You can use this public key or apply for your own key 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/2.deployment.md b/docs/Chinese/2.deployment.md index 91b1cf3a..a1e87b08 100644 --- a/docs/Chinese/2.deployment.md +++ b/docs/Chinese/2.deployment.md @@ -3,10 +3,226 @@ sort: 2 --- # 部署步骤 - -## google cloud -1. github页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如C:\KindleEa。 -2. 安装标准环境google cloud SDK/gloud CLI,并且执行 `gcloud init` + +## config.py +不管部署到什么平台,都先要正确配置config.py,这一节描述几个简单的配置项,其他配置项在下面章节详细描述。 + +| 配置项 | 含义 | +| ------------------- | -------------------------------------------------------- | +| APP_ID | 应用标识符,gae平台为应用ID,其他平台用于标识数据库等资源 | +| SRC_EMAIL | 发送邮件的发件人地址 | +| APP_DOMAIN | 应用部署后的域名 | +| TEMP_DIR | 制作电子书时的临时目录,为空则使用内存保存临时文件 | +| DOWNLOAD_THREAD_NUM | 下载网页的线程数量,需要目标平台支持多线程,最大值为5 | +| ALLOW_SIGNUP | 是否允许用户注册,False为需要管理员创建账号 | +| ADMIN_NAME | 管理员的账号名 | +| TIMEZONE | 新账号的默认时区,账号被创建后还可以在网页上自己修改时区 | +| POCKET_CONSUMER_KEY | 用于稍后阅读服务Pocket,可以用你自己的Key或就直接使用这个 | +| HIDE_MAIL_TO_LOCAL | 是否允许将生成的邮件保存到本地,用于调试或测试目的 | + + + + + +## 数据库选择 +数据库用于保存应用的配置数据和订阅数据。 +得益于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, 基本兼容了市面上的主流数据库,更适合全平台部署,平台支持什么数据库就可以使用什么数据库。 +本应用的数据量不大,确切的来说,很小很小,一般就几十行数据,选择什么数据库都不会对资源消耗和性能造成什么影响,即使最简单的就用一个文本文件当做数据库使用可能都会比其他正规的数据库要快。 + + + +### Datastore +Datastore为google的NoSQL数据库,我们要使用的是firebase的datastore模式。 +如果要部署到google cloud,基本上你只能选择datastore,因为它有免费额度。 +要使用datastore,参数配置如下: +``` +DATABASE_URL = 'datastore' +``` + + + +### SQLite +单文件数据库。适用于有本地文件系统读写权限的平台,特别是资源受限系统比如树莓派和各种派之类的。 +要使用datastore,参数配置如下: +``` +#template: +DATABASE_URL = 'sqlite:////path/to/database.db' +#examples: +DATABASE_URL = 'sqlite:////C:/Users/name/kindleear/site.db' +DATABASE_URL = 'sqlite:////home/username/dbfilename.db' +``` + + + +### MySQL/PostgreSQL/CockroachDB +典型企业级SQL数据库。大炮打蚊子,如果平台支持,直接使用也无妨。 +参数配置如下: +``` +#template: +DATABASE_URL = 'mysql://username:password@hostname:port/database_name' +DATABASE_URL = 'postgresql://username:password@hostname:port/database_name' + +#examples: +DATABASE_URL = 'mysql://root:password@localhost:3306/mydatabase' +DATABASE_URL = 'mysql://user:pass123@example.com:3306/mydatabase' +DATABASE_URL = 'postgresql://postgres:password@localhost:5432/mydatabase' +DATABASE_URL = 'postgresql://user:pass123@example.com:5432/mydatabase' + +import os +db_username = os.getenv('DB_USERNAME') +db_password = os.getenv('DB_PASSWORD') +db_host = os.getenv('DB_HOST') +db_port = os.getenv('DB_PORT') +db_name = os.getenv('DB_NAME') +database_url = f"mysql://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}" +``` + + + +### MongoDB +应用最广的典型NoSQL数据库。 +参数配置如下: +``` +#template: +DATABASE_URL = 'mongodb://username:password@hostname:port/' +#examples: +DATABASE_URL = 'mongodb://127.0.0.1:27017/' +DATABASE_URL = 'mongodb://user:pass123@example.com:27017/' +``` + + + +### Redis +可以持久化到磁盘的内存数据库。如果目标系统已经安装并使用了redis用于任务队列,则直接使用redis可以省去安装其他数据库的资源消耗,但使用前要做好相关的redis持久化配置,避免丢失数据。 +参数配置如下(db_number可以省略,如果是0,建议省略): +``` +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' +``` + + + +### Pickle +作者使用Python的pickle数据持久化标准库创建的一个非常简单的单文件"数据库",可以用于资源特别受限的系统或用于测试目的。 +参数配置如下: +``` +#template: +DATABASE_URL = 'pickle:////path/to/database.db' +#examples: +DATABASE_URL = 'pickle:////C:/Users/name/kindleear/site.db' +DATABASE_URL = 'pickle:////home/username/dbfilename.db' +``` + + + +## 任务队列和定时任务选择 +任务队列用于异步执行抓取网页内容、制作电子书、发送邮件等耗时任务。 +定时任务用于定时检查是否需要推送、清零过期推送记录等事项。 +### gae +如果要部署到google cloud,你只能选择gae。 +``` +TASK_QUEUE_SERVICE = "gae" +TASK_QUEUE_BROKER_URL = "" +``` + + + +### apscheduler +比较轻量,最简配置可以不依赖redis和其他数据库,直接使用内存保存任务状态,只是有一定的丢失任务风险,在任务队列执行过程中掉电重新上电后原任务不会重新运行,只能等新的任务时间到达。 +如果要使用数据库持久化,支持sqlite/mysql/postgresql/mongodb/redis,可以配置为DATABASE_URL相同的值。 +``` +TASK_QUEUE_SERVICE = "apscheduler" + +TASK_QUEUE_BROKER_URL = "" #use memory store +#or +TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/" +#or +TASK_QUEUE_BROKER_URL = "sqlite:////home/username/dbfilename.db" +``` + + + +### celery +最著名的任务队列,支持多种后端技术,包括redis、mongodb、sql、共享目录等,如果要使用数据库保存任务状态,可以配置为DATABASE_URL相同的值。 +``` +TASK_QUEUE_SERVICE = "celery" + +TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/" +#or +TASK_QUEUE_BROKER_URL = "sqlite:////home/username/dbfilename.db" +#or +TASK_QUEUE_BROKER_URL = "file:///var/celery/results/" #results is a directory +TASK_QUEUE_BROKER_URL = "file:////?/C:/Users/name/results/" #keep the prefix 'file:////?/' if in windows +``` + + + +### rq +比celery稍轻量的任务队列,依赖redis,需要额外安装redis服务。 +``` +TASK_QUEUE_SERVICE = "celery" +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平台有一个比较独特的服务,应用部署完成后除了可以发送邮件,还可以接收邮件,这个功能有时候还是比较有用的,具体的用法可以参考 [FAQ](3.faq.html#appspotmail)。 +如果要启用此服务,配置如下(注意:仅gae平台支持,其他平台设置了也没用): +``` +INBOUND_EMAIL_SERVICE = 'gae' +``` + + + + +## Web 服务器 +KindleEar使用Flask框架实现Web界面管理,入口点在 main.app (main.py文件内的app实例对象), +可以使用任何支持wsgi标准接口的Web服务器软件启动此app即可。 +要求不高的,使用Flask的调试服务器直接启动都可以。 +如果部署到google cloud,默认使用 Gunicorn,也可以任意切换为 uWSGI, Tornado, mod_wsgi 等。 +其他目标平台也是一样的做法,选择已有的或自己喜欢的就行。 + + + + +## requirements.txt +KindleEar使用requirements.txt管理各种库的依赖,在各种平台部署时都可以一行命令就完成应用的环境配置 +``` +pip install -r requirments.txt +``` +因各种配置组合较多,要手动配置requirements.txt比较复杂,容易出错。 +为此,作者提供了一个脚本文件tools/deploy_helper.py, +配置好config.py后,直接执行此文件就可以生成requirements.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.faq.md b/docs/Chinese/3.faq.md index 208841b1..80a1f8a8 100644 --- a/docs/Chinese/3.faq.md +++ b/docs/Chinese/3.faq.md @@ -34,6 +34,7 @@ cover_url = 'https://www.google.com/mycover.jpg' KindleEar不保存密码原文,无法取回密码。在登录时密码验证错误时会有一个“忘记密码?”链接,点击这个链接就可以使用创建账号时登记的email邮箱来重置密码,管理员的重置密码email邮箱就是config.py里面的SRC_EMAIL。 + ## xxx@appid.appspotmail.com 邮件地址怎么用? 如果您的应用是部署在Google cloud平台(GAE),KindleEar还额外赠送了一个邮件服务 xxx@appid.appspotmail.com ,在部署好KindleEar后,你自动拥有了无数个EMAIL邮箱地址,格式为:xxx@appid.appspotmail.com,xxx为任意合法字符串,appid为你的应用名。 diff --git a/docs/English/1.intro.md b/docs/English/1.intro.md index 63a6ccf7..3314e1d3 100644 --- a/docs/English/1.intro.md +++ b/docs/English/1.intro.md @@ -3,5 +3,10 @@ sort: 1 --- # Intro -KindleEar是开源免费网络应用,可以部署在大多数支持Python的托管平台,包括但不限于google appengine,Heroku,Pythonanywhere,主要功能是自动定期通过RSS收集网络文章然后制作成图文并茂的电子书推送至你的Kindle。 +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. + + + diff --git a/docs/English/2.deployment.md b/docs/English/2.deployment.md index 16df5e81..71f1ed80 100644 --- a/docs/English/2.deployment.md +++ b/docs/English/2.deployment.md @@ -1,7 +1,241 @@ --- sort: 2 --- -# 部署步骤 +# Deployment Steps -页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如C:\KindleEar(下面以这个为例)。 + +## config.py +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 | +| 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 | + + + + +## Database Selection +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. +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. + + + + +### 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: +``` +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: +``` +#template: +DATABASE_URL = 'sqlite:////path/to/database.db' +#examples: +DATABASE_URL = 'sqlite:////C:/Users/name/kindleear/site.db' +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: +``` +#template: +DATABASE_URL = 'mysql://username:password@hostname:port/database_name' +DATABASE_URL = 'postgresql://username:password@hostname:port/database_name' + +#examples: +DATABASE_URL = 'mysql://root:password@localhost:3306/mydatabase' +DATABASE_URL = 'mysql://user:pass123@example.com:3306/mydatabase' +DATABASE_URL = 'postgresql://postgres:password@localhost:5432/mydatabase' +DATABASE_URL = 'postgresql://user:pass123@example.com:5432/mydatabase' + +import os +db_username = os.getenv('DB_USERNAME') +db_password = os.getenv('DB_PASSWORD') +db_host = os.getenv('DB_HOST') +db_port = os.getenv('DB_PORT') +db_name = os.getenv('DB_NAME') +database_url = f"mysql://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}" +``` + + + +### MongoDB +The most widely used typical NoSQL database. +Parameter configuration is as follows: +``` +#template: +DATABASE_URL = 'mongodb://username:password@hostname:port/' +#examples: +DATABASE_URL = 'mongodb://127.0.0.1:27017/' +DATABASE_URL = 'mongodb://user:pass123@example.com:27017/' +``` + + + +### Redis +A memory 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): +``` +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' +``` + + + +### Pickle +A very simple single-file "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: +``` +#template: +DATABASE_URL = 'pickle:////path/to/database.db' +#examples: +DATABASE_URL = 'pickle:////C:/Users/name/kindleear/site.db' +DATABASE_URL = 'pickle:////home/username/dbfilename.db' +``` + + + + + +## Task Queue and Scheduler Selection +Task queues are used for asynchronously executing tasks such as fetching web content, creating eBooks, sending emails, etc. +Scheduler tasks are used for periodically checking whether there is a need for pushing notifications, resetting expired push records, etc. + + + +### gae +If you want to deploy to Google Cloud, you can only choose GAE. +``` +TASK_QUEUE_SERVICE = "gae" +TASK_QUEUE_BROKER_URL = "" +``` + + + +### apscheduler +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. +``` +TASK_QUEUE_SERVICE = "apscheduler" + +TASK_QUEUE_BROKER_URL = "" # use memory store +# or +TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/" +# or +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. +``` +TASK_QUEUE_SERVICE = "celery" + +TASK_QUEUE_BROKER_URL = "redis://127.0.0.1:6379/" +# or +TASK_QUEUE_BROKER_URL = "sqlite:////home/username/dbfilename.db" +# or +TASK_QUEUE_BROKER_URL = "file:///var/celery/results/" # results is a directory +TASK_QUEUE_BROKER_URL = "file:////?/C:/Users/name/results/" # keep the prefix 'file:////?/' if in windows +``` + + + +### rq +Slightly lighter-weight than Celery, it depends on Redis and requires an additional installation of the Redis service. +``` +TASK_QUEUE_SERVICE = "celery" +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. + + + + +## 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): +``` +INBOUND_EMAIL_SERVICE = 'gae' +``` + + + + +## Web Server +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. +If deploying to Google Cloud, it defaults to using Gunicorn, but you can freely switch to uWSGI, Tornado, mod_wsgi, etc. +The same applies to other target platforms; choose whichever you prefer or are familiar with. + + + + +## 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. +``` +pip install -r requirements.txt +``` + +Due to the various configuration combinations, manually configuring requirements.txt can be complex and prone to errors. +To address this, the author provides a script file `tools/deploy_helper.py`. +After configuring config.py, simply execute this file to generate requirements.txt. +Alternatively, you can choose not to use this script and remove all comments in requirements.txt, which installs all dependencies, as it won't take up much space anyway. + + + + + +## 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.faq.md b/docs/English/3.faq.md index 63c3f2f8..afdcfeaf 100644 --- a/docs/English/3.faq.md +++ b/docs/English/3.faq.md @@ -33,6 +33,7 @@ Additionally, if you want to customize the masthead, add a masthead_url attribut 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. + ## How to use the xxx@appid.appspotmail.com email address? If your application is deployed on the Google Cloud Platform (GAE), KindleEar also provides an additional email service xxx@appid.appspotmail.com. After deploying KindleEar, you automatically have countless EMAIL email addresses in the format: xxx@appid.appspotmail.com, where xxx is any legal string and appid is your application name. 1. To use this feature, you need to add whitelist items firstly. If it's '\*', it allows all emails. Otherwise, the format is 'xx@xx.xx' or '@xx.xx' (without single quotes). diff --git a/main.py b/main.py index 481227be..9f0f3ec7 100644 --- a/main.py +++ b/main.py @@ -23,22 +23,13 @@ def set_env(): else: os.environ['TEMP_DIR'] = os.path.join(appDir, TEMP_DIR) os.environ['DOWNLOAD_THREAD_NUM'] = str(int(DOWNLOAD_THREAD_NUM)) - os.environ['DATABASE_NAME'] = DATABASE_NAME - if '://' in DATABASE_NAME: - os.environ['DATABASE_ENGINE'] = DATABASE_NAME.split('://', 1)[0] - else: - os.environ['DATABASE_ENGINE'] = DATABASE_ENGINE - os.environ['DATABASE_HOST'] = DATABASE_HOST - os.environ['DATABASE_PORT'] = str(int(DATABASE_PORT)) - os.environ['DATABASE_USERNAME'] = DATABASE_USERNAME - os.environ['DATABASE_PASSWORD'] = DATABASE_PASSWORD - + 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['TASK_QUEUE_RESULT_BACKEND'] = TASK_QUEUE_RESULT_BACKEND os.environ['APP_DOMAIN'] = 'http://127.0.0.1:5000/' #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 '' set_env() @@ -74,7 +65,7 @@ def main(): print(result) return 0 - print('\nKindleEar Application') + print(f'\nKindleEar Application {appVer}') print('\nUsage: main.py [debug | deliver check | deliver now]') print('\ncommands:') print(' debug \t Run the application in debug mode') diff --git a/messages.pot b/messages.pot index a3bbda31..b58abd5f 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-17 14:29-0300\n" +"POT-Creation-Date: 2024-02-27 22:09-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -64,14 +64,18 @@ msgstr "" msgid "Invitation codes" msgstr "" +#: application/templates/admin.html:37 +msgid "one code per line" +msgstr "" + #: application/templates/admin.html:43 msgid "Accounts" msgstr "" #: application/templates/admin.html:43 #: application/templates/adv_whitelist.html:29 application/templates/my.html:32 -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add" msgstr "" @@ -473,7 +477,7 @@ msgstr "" msgid "Verified" msgstr "" -#: application/templates/base.html:59 application/view/login.py:73 +#: application/templates/base.html:59 application/view/login.py:75 #: application/view/share.py:145 msgid "The username does not exist or password is wrong." msgstr "" @@ -570,14 +574,14 @@ msgstr "" msgid "The account have been deleted." msgstr "" -#: application/templates/base.html:83 application/view/admin.py:208 +#: application/templates/base.html:83 application/view/admin.py:199 #: application/view/share.py:135 msgid "The username or password is empty." msgstr "" -#: application/templates/base.html:84 application/view/admin.py:85 -#: application/view/admin.py:172 application/view/admin.py:212 -#: application/view/login.py:207 application/view/login.py:267 +#: 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 msgid "The two new passwords are dismatch." msgstr "" @@ -589,7 +593,7 @@ msgstr "" msgid "Account added successfully." msgstr "" -#: application/templates/base.html:87 application/view/login.py:117 +#: application/templates/base.html:87 application/view/login.py:121 msgid "login required" msgstr "" @@ -701,8 +705,8 @@ msgstr "" msgid "Search" msgstr "" -#: application/templates/login.html:34 application/view/login.py:184 -#: application/view/login.py:191 +#: application/templates/login.html:34 application/view/login.py:188 +#: application/view/login.py:195 msgid "" "The website does not allow registration. You can ask the owner for an " "account." @@ -948,157 +952,157 @@ msgstr "" msgid "User account" msgstr "" -#: application/view/admin.py:50 application/view/setting.py:96 +#: application/view/admin.py:51 application/view/setting.py:96 msgid "Settings Saved!" msgstr "" -#: application/view/admin.py:60 application/view/admin.py:68 -#: application/view/admin.py:103 +#: application/view/admin.py:61 application/view/admin.py:69 +#: application/view/admin.py:94 msgid "Add account" msgstr "" -#: application/view/admin.py:67 application/view/admin.py:116 -#: application/view/admin.py:144 +#: application/view/admin.py:68 application/view/admin.py:107 +#: application/view/admin.py:135 msgid "You do not have sufficient privileges." msgstr "" -#: application/view/admin.py:81 application/view/admin.py:159 -#: application/view/login.py:203 application/view/login.py:232 +#: application/view/admin.py:82 application/view/admin.py:150 +#: application/view/login.py:207 application/view/login.py:236 #: application/view/setting.py:61 application/view/setting.py:63 #: application/view/setting.py:65 application/view/share.py:35 msgid "Some parameters are missing or wrong." msgstr "" -#: application/view/admin.py:83 application/view/login.py:42 -#: application/view/login.py:209 +#: application/view/admin.py:84 application/view/login.py:43 +#: application/view/login.py:213 msgid "The username includes unsafe chars." msgstr "" -#: application/view/admin.py:87 application/view/login.py:211 +#: application/view/admin.py:88 application/view/login.py:215 msgid "Already exist the username." msgstr "" -#: application/view/admin.py:93 application/view/admin.py:178 -#: application/view/admin.py:205 application/view/login.py:258 +#: application/view/admin.py:91 application/view/admin.py:169 +#: application/view/admin.py:196 application/view/login.py:262 msgid "The password includes non-ascii chars." msgstr "" -#: application/view/admin.py:120 application/view/admin.py:141 -#: application/view/admin.py:170 +#: application/view/admin.py:111 application/view/admin.py:132 +#: application/view/admin.py:161 msgid "The username '{}' does not exist." msgstr "" -#: application/view/admin.py:136 +#: application/view/admin.py:127 msgid "The password will not be changed if the fields are empties." msgstr "" -#: application/view/admin.py:137 application/view/admin.py:195 +#: application/view/admin.py:128 application/view/admin.py:186 msgid "Change account" msgstr "" -#: application/view/admin.py:138 application/view/admin.py:196 +#: application/view/admin.py:129 application/view/admin.py:187 msgid "Change" msgstr "" -#: application/view/admin.py:193 +#: application/view/admin.py:184 msgid "Change success." msgstr "" -#: application/view/admin.py:210 +#: application/view/admin.py:201 msgid "The old password is wrong." msgstr "" -#: application/view/admin.py:217 +#: application/view/admin.py:208 msgid "Change password success." msgstr "" -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 -#: application/view/adv.py:74 application/view/adv.py:75 #: 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 msgid "Append hyperlink '{}' to article" msgstr "" -#: application/view/adv.py:70 application/view/adv.py:71 -#: application/view/adv.py:72 application/view/adv.py:73 +#: application/view/adv.py:76 application/view/adv.py:77 +#: application/view/adv.py:78 application/view/adv.py:79 msgid "Save to {}" msgstr "" -#: application/view/adv.py:70 +#: application/view/adv.py:76 msgid "evernote" msgstr "" -#: application/view/adv.py:71 +#: application/view/adv.py:77 msgid "wiz" msgstr "" -#: application/view/adv.py:72 +#: application/view/adv.py:78 msgid "pocket" msgstr "" -#: application/view/adv.py:73 +#: application/view/adv.py:79 msgid "instapaper" msgstr "" -#: application/view/adv.py:74 application/view/adv.py:75 -#: application/view/adv.py:76 application/view/adv.py:77 -#: application/view/adv.py:78 +#: 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 "" -#: application/view/adv.py:74 +#: application/view/adv.py:80 msgid "weibo" msgstr "" -#: application/view/adv.py:75 +#: application/view/adv.py:81 msgid "tencent weibo" msgstr "" -#: application/view/adv.py:76 +#: application/view/adv.py:82 msgid "facebook" msgstr "" -#: application/view/adv.py:78 +#: application/view/adv.py:84 msgid "tumblr" msgstr "" -#: application/view/adv.py:79 +#: application/view/adv.py:85 msgid "Open in browser" msgstr "" -#: application/view/adv.py:352 +#: application/view/adv.py:359 msgid "Authorization Error!
{}" msgstr "" -#: application/view/adv.py:375 +#: application/view/adv.py:382 msgid "Success authorized by Pocket!" msgstr "" -#: application/view/adv.py:381 +#: application/view/adv.py:388 msgid "" "Failed to request authorization of Pocket!
See details " "below:

{}" msgstr "" -#: application/view/adv.py:391 +#: application/view/adv.py:398 msgid "Request type [{}] unsupported" msgstr "" -#: application/view/adv.py:406 +#: application/view/adv.py:413 msgid "The Instapaper service encountered an error. Please try again later." msgstr "" -#: application/view/deliver.py:68 application/view/login.py:154 +#: application/view/deliver.py:67 application/view/login.py:158 #: application/view/share.py:39 msgid "The username does not exist or the email is empty." msgstr "" -#: application/view/deliver.py:84 +#: application/view/deliver.py:83 msgid "The following recipes has been added to the push queue." msgstr "" -#: application/view/deliver.py:87 +#: application/view/deliver.py:86 msgid "There are no recipes to deliver." msgstr "" @@ -1106,79 +1110,79 @@ msgstr "" msgid "Cannot fetch data from {}, status: {}" msgstr "" -#: application/view/library.py:48 application/view/subscribe.py:192 -#: application/view/subscribe.py:304 application/view/subscribe.py:333 -#: application/view/subscribe.py:341 +#: 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 msgid "The recipe does not exist." msgstr "" -#: application/view/login.py:25 +#: application/view/login.py:26 msgid "Please use {}/{} to login at first time." msgstr "" -#: application/view/login.py:38 +#: application/view/login.py:39 msgid "Username is empty." msgstr "" -#: application/view/login.py:40 +#: application/view/login.py:41 msgid "The len of username reached the limit of 25 chars." msgstr "" -#: application/view/login.py:74 +#: application/view/login.py:76 msgid "Forgot password?" msgstr "" -#: application/view/login.py:133 application/view/login.py:269 +#: application/view/login.py:137 application/view/login.py:273 msgid "The token is wrong or expired." msgstr "" -#: application/view/login.py:136 +#: application/view/login.py:140 msgid "Please input the correct username and email to reset password." msgstr "" -#: application/view/login.py:138 +#: application/view/login.py:142 msgid "The email of account '{name}' is {email}." msgstr "" -#: application/view/login.py:159 +#: application/view/login.py:163 msgid "Reset password success, Please close this page and login again." msgstr "" -#: application/view/login.py:162 +#: application/view/login.py:166 msgid "The email you input is not associated with this account." msgstr "" -#: application/view/login.py:173 +#: application/view/login.py:177 msgid "The link to reset your password has been sent to your email." msgstr "" -#: application/view/login.py:174 +#: application/view/login.py:178 msgid "Please check your email inbox within 24 hours." msgstr "" -#: application/view/login.py:205 +#: application/view/login.py:209 msgid "The invitation code is invalid." msgstr "" -#: application/view/login.py:213 +#: application/view/login.py:217 msgid "" "Failed to create an account. Please contact the administrator for " "assistance." msgstr "" -#: application/view/login.py:223 +#: application/view/login.py:227 msgid "Successfully created account." msgstr "" -#: application/view/login.py:234 +#: application/view/login.py:238 msgid "Reset KindleEar password" msgstr "" -#: application/view/login.py:235 +#: application/view/login.py:239 msgid "This is an automated email. Please do not reply to it." msgstr "" -#: application/view/login.py:236 +#: application/view/login.py:240 msgid "You can click the following link to reset your KindleEar password." msgstr "" @@ -1294,7 +1298,7 @@ msgstr "" msgid "Hausa" msgstr "" -#: application/view/share.py:50 application/view/subscribe.py:240 +#: application/view/share.py:50 application/view/subscribe.py:239 msgid "Unknown command: {}" msgstr "" @@ -1349,41 +1353,41 @@ msgstr "" msgid "Failed to fetch the recipe." msgstr "" -#: application/view/subscribe.py:123 application/view/subscribe.py:265 +#: application/view/subscribe.py:123 application/view/subscribe.py:264 msgid "Failed to save the recipe. Error:" msgstr "" -#: application/view/subscribe.py:221 +#: application/view/subscribe.py:220 msgid "You can only delete the uploaded recipe." msgstr "" -#: application/view/subscribe.py:225 +#: application/view/subscribe.py:224 msgid "The recipe have been subscribed, please unsubscribe it before delete." msgstr "" -#: application/view/subscribe.py:238 +#: application/view/subscribe.py:237 msgid "This recipe has not been subscribed to yet." msgstr "" -#: application/view/subscribe.py:252 +#: application/view/subscribe.py:251 msgid "Can not read uploaded file, Error:" msgstr "" -#: application/view/subscribe.py:260 +#: application/view/subscribe.py:259 msgid "" "Failed to decode the recipe. Please ensure that your recipe is saved in " "utf-8 encoding." msgstr "" -#: application/view/subscribe.py:280 +#: application/view/subscribe.py:279 msgid "The recipe is already in the library." msgstr "" -#: application/view/subscribe.py:311 +#: application/view/subscribe.py:309 msgid "The login information for this recipe has been cleared." msgstr "" -#: application/view/subscribe.py:315 +#: application/view/subscribe.py:313 msgid "The login information for this recipe has been saved." msgstr "" diff --git a/requirements.txt b/requirements.txt index 88b69a98..e234ecc0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,17 +15,20 @@ flask-babel>=4.0.0,<=4.99.0 six>=1.0.0,<=1.99.0 feedparser>=6.0.0,<=6.99.0 -peewee>=3.0.0,<=3.99.0 -pymysql>=1.0.0,<=1.99.0 -#weedata>=0.1.0,<=0.99.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-rq2>=18.0,<=18.99 +flask-apscheduler>=1.0.0,<=1.99.0 #google-cloud-tasks>=2.0.0,<=2.99.0 -#flask-apscheduler>=1.0.0,<=1.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 diff --git a/tests/runtests.py b/tests/runtests.py index 6db0ad9f..63f1a5f9 100644 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -23,48 +23,28 @@ def set_env(): else: os.environ['TEMP_DIR'] = os.path.join(appDir, TEMP_DIR) os.environ['DOWNLOAD_THREAD_NUM'] = str(int(DOWNLOAD_THREAD_NUM)) - os.environ['DATABASE_ENGINE'] = 'sqlite' - os.environ['DATABASE_NAME'] = ':memory:' - os.environ['DATABASE_HOST'] = 'localhost' - os.environ['DATABASE_PORT'] = str(8081) - os.environ['DATABASE_USERNAME'] = '' - os.environ['DATABASE_PASSWORD'] = '' - + os.environ['DATABASE_URL'] = 'sqlite://:memory:' os.environ['TASK_QUEUE_SERVICE'] = TASK_QUEUE_SERVICE os.environ['TASK_QUEUE_BROKER_URL'] = TASK_QUEUE_BROKER_URL - os.environ['TASK_QUEUE_RESULT_BACKEND'] = TASK_QUEUE_RESULT_BACKEND 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 '' set_env() -if 1: - TEST_MODULES = ['test_login', 'test_setting', 'test_admin', 'test_subscribe', 'test_adv', - 'test_logs'] #'test_share', - if INBOUND_EMAIL_SERVICE == 'gae': - TEST_MODULES.append('test_inbound_email') -else: - TEST_MODULES = ['test_login'] - def runtests(suite, verbosity=1, failfast=False): runner = unittest.TextTestRunner(verbosity=verbosity, failfast=failfast) results = runner.run(suite) return results.failures, results.errors -def collect_tests(args=None): +def collect_tests(module=None): suite = unittest.TestSuite() - - if not args: - for m in [reload_module(m) for m in TEST_MODULES]: - module_suite = unittest.TestLoader().loadTestsFromModule(m) - suite.addTest(module_suite) - else: - for arg in args: - m = reload_module(arg) - user_suite = unittest.TestLoader().loadTestsFromNames(m) - suite.addTest(user_suite) - + modules = [module] if module else TEST_MODULES + for target in modules: + m = reload_module(target) + user_suite = unittest.TestLoader().loadTestsFromModule(m) + suite.addTest(user_suite) return suite def reload_module(module_name): @@ -72,30 +52,35 @@ def reload_module(module_name): #importlib.reload(module) return module -def main(): - verbosity = 1 #Verbosity of output, 0 | 1 | 4 - failfast = 0 #Exit on first failure/error - report = '' # '' | 'html' | 'console' - - os.environ['KE_TEST_VERBOSITY'] = str(verbosity) - os.environ['KE_SLOW_TESTS'] = '1' #Run tests that may be slow - +def start_test(verbosity=1, failfast=0, testonly='', report=''): if report: cov = coverage.coverage() cov.start() - suite = collect_tests() - runtests(suite, verbosity, failfast) + runtests(collect_tests(), verbosity, failfast) if report: cov.stop() cov.save() if report == 'html': - cov.html_report(directory=os.path.join(testDir, 'cov_html')) + cov.html_report(directory=os.path.join(testDir, 'covhtml')) else: cov.report() return 0 +TEST_MODULES = ['test_login', 'test_setting', 'test_admin', 'test_subscribe', 'test_adv', + 'test_logs'] #'test_share', +if INBOUND_EMAIL_SERVICE == 'gae': + TEST_MODULES.append('test_inbound_email') + if __name__ == '__main__': - sys.exit(main()) + verbosity = 1 #Verbosity of output, 0 | 1 | 4 + failfast = 0 #Exit on first failure/error + report = '' # '' | 'html' | 'console' + testonly = '' #module name, empty for testing all + + os.environ['KE_TEST_VERBOSITY'] = str(verbosity) + os.environ['KE_SLOW_TESTS'] = '1' #Run tests that may be slow + sys.exit(start_test(verbosity, failfast, testonly, report)) + diff --git a/tools/deploy_helper.py b/tools/deploy_helper.py index 6d7ddb02..a98fba6f 100644 --- a/tools/deploy_helper.py +++ b/tools/deploy_helper.py @@ -19,7 +19,8 @@ ('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'),] + ('feedparser', '>=6.0.0,<=6.99.0'), +] REQ_DB = { 'datastore': [('weedata', '>=0.1.0,<=0.99.0'), ('google-cloud-datastore', '>=2.19.0,<=2.99.0'),], @@ -28,7 +29,9 @@ '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'),],} + 'redis': [('weedata', '>=0.1.0,<=0.99.0'), ('redis', '>=4.5.0,<=5.99.0'),], + 'pickle': [('weedata', '>=0.1.0,<=0.99.0'),], +} REQ_TASK = { 'gae': [('google-cloud-tasks', '>=2.0.0,<=2.99.0'),], @@ -39,17 +42,23 @@ REQ_PLAT = {'gae': [('appengine-python-standard', '>=1.1.0,<=1.99.0'),],} -def write_req(reqFile, db, task, plat): +EXTRA = { + 'sqlalchemy': [('sqlalchemy', '')], +} + +def write_req(reqFile, db, task, plat, *extra): with open(reqFile, 'w', encoding='utf-8') as f: f.write('\n'.join([''.join(item) for item in REQ_COMM])) f.write('\n') - for req, opt in zip([REQ_DB, REQ_TASK, REQ_PLAT], [db, task, plat]): + EXTRAS = [EXTRA for idx in range(len(extra))] + for req, opt in zip([REQ_DB, REQ_TASK, REQ_PLAT, *EXTRAS], [db, task, plat, *extra]): 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(''.join(item) + '\n') for key, items in req.items(): if key != opt: for item in filter(lambda x: x[0] not in seen, (items or [])): @@ -57,26 +66,33 @@ def write_req(reqFile, db, task, plat): f.write('#' + ''.join(item) + '\n') #parse config.py to a string with format symbols -def config_to_fmtstr(cfgFile): +def config_to_fmtstr(cfgFile, fmt='dict'): with open(cfgFile, 'r', encoding='utf-8') as f: lines = f.read().splitlines() - ret = [] + ret = [] if fmt == 'list' else {} docComment = False - pattern = r"""^([_A-Z]+)\s*=\s*("[^"]*"|'[^']*'|\S+)\s*(#.*)?$""" + 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 - ret.append(line) + if fmt == 'list': + ret.append(line) continue elif not line or line.startswith('#') or docComment: - ret.append(line) + if fmt == 'list': + ret.append(line) continue match = re.match(pattern, line) if match: - ret.append((match.group(1), match.group(2), match.group(3))) + 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':,...} @@ -107,6 +123,27 @@ def write_cfg(cfgFile, cfgItems): else: f.write(f'{item} = {orgValue}{comment}\n') -#write_cfg(r'D:\Programer\Project\KindleEar\config.py', {'APP_ID': 'suqiyuan', 'DATABASE_PORT': 99, 'ALLOW_SIGNUP': True}) +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') + + cfg = config_to_fmtstr(cfgFile) + db = cfg['DATABASE_URL'].split('://')[0] + task = cfg['TASK_QUEUE_SERVICE'] + broker = cfg['TASK_QUEUE_BROKER_URL'] + if (cfg['DATABASE_URL'].startswith('datastore') or cfg['INBOUND_EMAIL_SERVICE'] == 'gae' or + cfg['TASK_QUEUE_SERVICE'] == 'gae'): + plat = 'gae' + else: + plat = '' + extras = set() + if broker.startswith('redis://'): + extras.add('redis') + elif broker.startswith('mongodb://'): + extras.add('pymongo') + elif broker.startswith(('sqlite://', 'mysql://', 'postgresql://')): + extras.add('sqlalchemy') + write_req(reqFile, db, task, plat, *extras) -#write_req(r'D:\Programer\Project\KindleEar\requirements.txt', 'mysql', 'rq', 'gae')