diff --git a/.gitignore b/.gitignore index 77188e44..b620f04a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ tests/rss/* tests/debug_mail/* tests/cov_html/* .idea/ +datastore/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..c3e1322a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 cdhigh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app.yaml b/app.yaml index 104c866c..425732f3 100755 --- a/app.yaml +++ b/app.yaml @@ -1,4 +1,5 @@ runtime: python38 +service: default #B1: 384MB/600MHz/manual_scaling instance_class: B1 @@ -23,21 +24,19 @@ inbound_services: handlers: - url: /robots.txt - static_files: static/robots.txt - upload: static/robots.txt + static_files: application/static/robots.txt + upload: application/static/robots.txt - url: /favicon.ico - static_files: static/favicon.ico - upload: static/favicon.ico + static_files: application/static/favicon.ico + upload: application/static/favicon.ico mime_type: image/x-icon - url: /static - static_dir: static - application_readable: true + static_dir: application/static - url: /images - static_dir: images - application_readable: true + static_dir: application/images - url: /.* secure: always diff --git a/application/__init__.py b/application/__init__.py index 8ace4219..a474535e 100644 --- a/application/__init__.py +++ b/application/__init__.py @@ -4,7 +4,7 @@ # Author: cdhigh __Author__ = "cdhigh" -__Version__ = '3.0.0' +__Version__ = '3.0.0b' import os, builtins, datetime from flask import Flask, render_template, session, request, g @@ -22,12 +22,8 @@ def init_app(name, debug=False): app = Flask(name, template_folder=template_folder, static_folder=static_folder) app.config.from_pyfile(os.path.join(rootDir, 'config.py')) - - from .utils import new_secret_key - - app.config['SECRET_KEY'] = '12345678' # if debug else new_secret_key() app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024 #32MB - + from .view import setting app.config["BABEL_TRANSLATION_DIRECTORIES"] = i18n_folder babel = Babel(app) diff --git a/application/back_end/db_models.py b/application/back_end/db_models.py index 8fd49681..3f0a7b49 100644 --- a/application/back_end/db_models.py +++ b/application/back_end/db_models.py @@ -17,11 +17,12 @@ class KeUser(MyBaseModel): # kindleEar User name = CharField(unique=True) passwd = CharField() email = CharField() + sender = CharField() #可能等于自己的email,也可能是管理员的email secret_key = CharField(default='') kindle_email = CharField(default='') enable_send = BooleanField(default=False) send_days = JSONField(default=JSONField.list_default) - send_time = IntegerField(default=0) + send_time = IntegerField(default=6) timezone = IntegerField(default=0) expiration_days = IntegerField(default=0) #账号超期设置值,0为永久有效 expires = DateTimeField(null=True) #超过了此日期后账号自动停止推送 @@ -114,7 +115,7 @@ class UserBlob(MyBaseModel): name = CharField() user = CharField() time = DateTimeField(default=datetime.datetime.utcnow) - data = BlobField(null=True) + data = BlobField(null=True, index=False) #RSS订阅源,包括自定义RSS,上传的recipe,内置在zip里面的builtin_recipe不包括在内 #每个Recipe的字符串表示为:custom:id, upload:id @@ -125,7 +126,7 @@ class Recipe(MyBaseModel): isfulltext = BooleanField(default=False) #只有自定义RSS才有意义 type_ = CharField() #'custom','upload' needs_subscription = BooleanField(default=False) #是否需要登陆网页,只有上传的recipe才有意义 - src = TextField(default='') #保存上传的recipe的unicode字符串表示,已经解码 + src = TextField(default='', index=False) #保存上传的recipe的unicode字符串表示,已经解码 time = DateTimeField() #源被加入的时间,用于排序 user = CharField() #哪个账号创建的,和nosql一致,保存用户名 language = CharField(default='') @@ -176,7 +177,7 @@ class DeliverLog(MyBaseModel): to = CharField() size = IntegerField(default=0) time_str = CharField() #每个用户的时区可能不同,为显示方便,创建记录时就生成推送时间字符串 - datetime = DateTimeField(index=True) + datetime = DateTimeField() book = CharField(default='') status = CharField() @@ -193,7 +194,7 @@ class SharedRss(MyBaseModel): language = CharField(default='') category = CharField(default='') recipe_url = CharField(default='') #客户端优先使用此字段获取recipe,为什么不用上面的url是要和以前的版本兼容 - src = TextField(default='') #保存分享的recipe的unicode字符串表示,已经解码 + src = TextField(default='', index=False) #保存分享的recipe的unicode字符串表示,已经解码 description = CharField(default='') creator = CharField(default='') #保存贡献者的md5 created_time = DateTimeField(default=datetime.datetime.utcnow) @@ -246,4 +247,4 @@ def create_database_tables(): SharedRss, SharedRssCategory, LastDelivered, AppInfo], safe=True) #close_database() - #print(f'Create database "{dbName}" finished') + return 'Created database tables successfully' diff --git a/application/back_end/db_models_nosql.py b/application/back_end/db_models_nosql.py index b1b041ea..fdd7469e 100644 --- a/application/back_end/db_models_nosql.py +++ b/application/back_end/db_models_nosql.py @@ -7,7 +7,7 @@ from weedata import * dbUrl = os.getenv('DATABASE_URL') -appId = os.getenv('APP_ID', 'kindleear') +appId = os.getenv('APP_ID') if dbUrl.startswith('mongodb://'): dbInstance = MongoDbClient(appId, dbUrl) diff --git a/application/back_end/send_mail_adpt.py b/application/back_end/send_mail_adpt.py index ad1c5401..21d52341 100644 --- a/application/back_end/send_mail_adpt.py +++ b/application/back_end/send_mail_adpt.py @@ -5,16 +5,21 @@ #gae mail api #https://cloud.google.com/appengine/docs/standard/python3/reference/services/bundled/google/appengine/api/mail #https://cloud.google.com/appengine/docs/standard/python3/services/mail -import os, datetime, zipfile +import os, datetime, zipfile, base64 from ..utils import local_time, ke_decrypt from ..base_handler import save_delivery_log from .db_models import KeUser -try: - from google.appengine.api import mail as gae_mail - from google.appengine.api.mail_errors import InvalidSenderError, InvalidAttachmentTypeError, InvalidEmailError - from google.appengine.runtime.apiproxy_errors import OverQuotaError, DeadlineExceededError -except ImportError: +#google.appengine will apply patch for os.env module +hideMailLocal = os.getenv('HIDE_MAIL_TO_LOCAL') + +#判断是否是部署在gae平台 +if os.getenv('DATABASE_URL') == 'datastore': + try: + from google.appengine.api import mail as gae_mail + except ImportError: + gae_mail = None +else: gae_mail = None try: @@ -23,6 +28,11 @@ except ImportError: SendGridAPIClient = None +try: + from mailjet_rest import Client as MailjetClient +except ImportError: + MailjetClient = None + try: from smtp_mail import smtp_send_mail except ImportError: @@ -35,9 +45,11 @@ def avaliable_sm_services(): sm.append('gae') if SendGridAPIClient: sm.append('sendgrid') + if MailjetClient: + sm.append('mailjet') if smtp_send_mail: sm.append('smtp') - if not os.getenv('HIDE_MAIL_TO_LOCAL'): + if not hideMailLocal: sm.append('local') return sm @@ -53,27 +65,28 @@ def send_to_kindle(user, title, attachment, fileWithTime=True): lcTime = "({})".format(lcTime) if fileWithTime else "" fileName = f"{title}{lcTime}.{user.book_type}" attachment = (fileName, attachment) - - if not isinstance(attachment, list): - attachments = [attachment] + if not isinstance(attachment, list): + attachment = [attachment] + status = 'ok' body = "Deliver from KindleEar" try: - send_mail(user, user.kindle_email, subject, body, attachments) + send_mail(user, user.kindle_email, subject, body, attachment) except Exception as e: status = str(e) default_log.warning(f'Failed to send mail "{title}": {status}') - save_delivery_log(user, title, len(attachment), status=status) + size = sum([len(a[1]) for a in attachment]) + save_delivery_log(user, title, size, status=status) #统一的发送邮件函数 def send_mail(user, to, subject, body, attachments=None, html=None): - if not isinstance(to, list) and (',' in to): + if not isinstance(to, list): to = to.split(',') sm_service = user.get_send_mail_service() - srv_type = sm_service.get('service', 'gae') - data = {'sender': os.getenv('SRC_EMAIL'), 'to': to, 'subject': subject, 'body': body} + srv_type = sm_service.get('service', '') + data = {'sender': user.sender, 'to': to, 'subject': subject, 'body': body} if attachments: data['attachments'] = attachments if html: @@ -85,10 +98,14 @@ def send_mail(user, to, subject, body, attachments=None, html=None): elif srv_type == 'sendgrid': apikey = sm_service.get('apikey', '') grid_send_mail(apikey=apikey, **data) + elif srv_type == 'mailjet': + apikey = sm_service.get('apikey', '') + secret_key = sm_service.get('secret_key', '') + mailjet_send_mail(apikey=apikey, secret_key=secret_key, **data) elif srv_type == 'smtp': data['host'] = sm_service.get('host', '') data['port'] = sm_service.get('port', 587) - data['username'] = sm_service.get('username', '') + data['username'] = user.sender data['password'] = ke_decrypt(sm_service.get('password', ''), user.secret_key) smtp_send_mail(**data) elif srv_type == 'local': @@ -98,7 +115,7 @@ def send_mail(user, to, subject, body, attachments=None, html=None): #发送一个HTML邮件 #user: KeUser实例 -#to: 收件地址,可以是一个单独的字符串,或一个字符串列表,对应发送到多个地址 +#to: 收件地址列表 #subject: 邮件标题 #html: 邮件正文的HTML内容 #attachments: 附件文件名和二进制内容,[(fileName, content),...] @@ -120,12 +137,11 @@ def send_html_mail(user, to, subject, html, attachments=None, body=None): #SendGrid发送邮件 #sender:: 发送者地址 -#to: 收件地址,可以是一个单独的字符串,或一个字符串列表,对应发送到多个地址 +#to: 收件地址列表 #subject: 邮件标题 #body: 邮件正文纯文本内容 #html: 邮件正文HTML #attachment: [(fileName, attachment),] -#tz: 时区 def grid_send_mail(apikey, sender, to, subject, body, html=None, attachments=None): global default_log sgClient = SendGridAPIClient(apikey) @@ -146,6 +162,43 @@ def grid_send_mail(apikey, sender, to, subject, body, html=None, attachments=Non response = sgClient.send(message) if response.status_code not in (200, 202): raise Exception(f'sendgrid failed: {response.status_code}') + +#Mailjet发送邮件 +#sender:: 发送者地址 +#to: 收件地址列表 +#subject: 邮件标题 +#body: 邮件正文纯文本内容 +#html: 邮件正文HTML +#attachment: [(fileName, attachment),] +def mailjet_send_mail(apikey, secret_key, sender, to, subject, body, html=None, attachments=None): + global default_log + mjClient = MailjetClient(auth=(apikey, secret_key), version='v3.1') + to = [{'Email': t, 'Name': t} for t in to] + data = {'Messages': [{ + "From": {"Email": sender, "Name": sender}, + "To": to, + "Subject": subject, + "TextPart": body, + }],} + + dataDict = data['Messages'][0] + if html: + dataDict['HTMLPart'] = html + if attachments: + dataDict['Attachments'] = [] + for fileName, content in (attachments or []): + dataDict['Attachments'].append({"ContentType": "text/plain", "Filename": fileName, + "Base64Content": base64.b64encode(content).decode()}) + + + resp = mjClient.send.create(data=data) + if resp.status_code in (200, 202): + status = resp.json()["Messages"][0]["Status"] + print(resp.json()) #TODO + if status != "success": + raise Exception(f'mailjet failed: {status}') + else: + raise Exception(f'mailjet failed: {resp.status_code}') def save_mail_to_local(dest_dir, subject, body, attachments=None, html=None, **kwargs): attachments = attachments or [] diff --git a/application/back_end/task_queue_apscheduler.py b/application/back_end/task_queue_apscheduler.py index 022e3b48..63f750de 100644 --- a/application/back_end/task_queue_apscheduler.py +++ b/application/back_end/task_queue_apscheduler.py @@ -23,7 +23,7 @@ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore jobstores = {"default": SQLAlchemyJobStore(url=_broker_url)} elif _broker_url == '': - jobstores = None #default is memory store + jobstores = {} #default is memory store else: raise ValueError('Unsupported TASK_QUEUE_BROKER_URL type: {_broker_url}') diff --git a/application/back_end/task_queue_gae.py b/application/back_end/task_queue_gae.py index 2958fa0a..10bde17b 100644 --- a/application/back_end/task_queue_gae.py +++ b/application/back_end/task_queue_gae.py @@ -1,10 +1,15 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- -#任务队列GAE +#任务队列GAE接口实现 #Author: cdhigh -import json +import os, json +from urllib.parse import urlencode, urlparse +appId = os.getenv('APP_ID') +serverLoc = os.getenv('SERVER_LOCATION') + from google.cloud import tasks_v2 DEFAULT_QUEUE_NAME = "default" +TASK_HTTP_METHOD = tasks_v2.HttpMethod.GET def init_task_queue_service(app): pass @@ -19,22 +24,18 @@ def create_url2book_task(payload: dict): #创建一个任务 #url: 任务要调用的链接 #payload: 要传递给url的参数,为一个Python字典 +#之所以使用HTTP.GET但是这里传入的是payload,而不是在调用方将payload合入url是为了兼容各种任务队列实现 #返回创建的任务实例 def create_http_task(url, payload): client = tasks_v2.CloudTasksClient() + taskParent = client.queue_path(appId, serverLoc, DEFAULT_QUEUE_NAME) - task = {"app_engine_http_request": { - "http_method": tasks_v2.HttpMethod.GET, - "relative_uri": url,} - } - if payload: - task["app_engine_http_request"]["headers"] = {"Content-type": "application/json"} - task["app_engine_http_request"]["body"] = json.dumps(payload).encode() - return client.create_task(task=task) + task = {"app_engine_http_request": {"http_method": TASK_HTTP_METHOD}} - #httpRequest = tasks_v2.HttpRequest(http_method=tasks_v2.HttpMethod.GET, url=url, - # headers={"Content-type": "application/json"}, body=json.dumps(payload).encode(),) - #task = tasks_v2.Task(httpRequest=httpRequest) - #taskParent = client.queue_path(APP_ID, SERVER_LOCATION, DEFAULT_QUEUE_NAME) - #return client.create_task(tasks_v2.CreateTaskRequest(parent=taskParent, task=task)) - + if TASK_HTTP_METHOD == tasks_v2.HttpMethod.GET: #转换字典为查询字符串 + params = {'relative_uri': urlparse(url)._replace(query=urlencode(payload, doseq=True)).geturl()} + else: #转换字典为post的body内容 + params = {'relative_uri': url, 'headers': {"Content-type": "application/json"}, 'body': json.dumps(payload).encode()} + + task["app_engine_http_request"].update(params) + return client.create_task(request={'parent': taskParent, 'task': task}) diff --git a/application/base_handler.py b/application/base_handler.py index 34854bbc..c077ccdd 100644 --- a/application/base_handler.py +++ b/application/base_handler.py @@ -17,7 +17,7 @@ def login_required(forAjax=False): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): - if ((session.get('login') == 1) and get_login_user()): + if (session.get('login', '') == 1) and session.get('userName', ''): return func(*args, **kwargs) else: return redirect(url_for("bpLogin.NeedLoginAjax") if forAjax else url_for("bpLogin.Login")) @@ -27,7 +27,8 @@ def wrapper(*args, **kwargs): #查询当前登录用户名,在使用此函数前最好保证已经登录 #返回一个数据库行实例,而不是一个字符串 def get_login_user(): - return KeUser.get_or_none(KeUser.name == session.get('userName', '')) + name = session.get('userName', '') + return KeUser.get_or_none(KeUser.name == name) if name else None #记录投递记录到数据库 def save_delivery_log(user, book, size, status='ok', to=None): diff --git a/application/lib/build_ebook.py b/application/lib/build_ebook.py index fd02abbd..d935b10d 100644 --- a/application/lib/build_ebook.py +++ b/application/lib/build_ebook.py @@ -68,8 +68,8 @@ def ke_opts(user, options=None): options.setdefault('dont_split_on_page_breaks', True) options['user'] = user - options.setdefault('debug_pipeline', os.getenv('TEMP_DIR')) - options.setdefault('verbose', 1) - options.setdefault('test', 1) + #options.setdefault('debug_pipeline', os.getenv('TEMP_DIR')) + #options.setdefault('verbose', 1) + #options.setdefault('test', 1) return options diff --git a/application/lib/calibre/web/feeds/news.py b/application/lib/calibre/web/feeds/news.py index aefbb444..26bbb599 100644 --- a/application/lib/calibre/web/feeds/news.py +++ b/application/lib/calibre/web/feeds/news.py @@ -1099,7 +1099,7 @@ def append_share_links(self, soup, url): ashare.string = _('Save to {}').format(type_) aTags.append(ashare) - for type_ in ['Weibo', 'TencentWeibo', 'Facebook', 'X', 'Tumblr']: + for type_ in ['Weibo', 'Facebook', 'X', 'Tumblr']: if shareLinks.get(type_): ashare = soup.new_tag('a', href=self.make_share_link(type_, user, url, soup)) ashare.string = _('Share on {}').format(type_) @@ -1127,17 +1127,15 @@ def make_share_link(self, shareType, user, url, soup): if shareType in ('Evernote', 'Wiz'): href = f"{appDomain}/share?act={shareType}&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}" elif shareType == 'Pocket': - href = f'{appDomain}/share?act=pocket&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}' + href = f'{appDomain}/share?act=Pocket&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}' elif shareType == 'Instapaper': - href = f'{appDomain}/share?act=instapaper&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}' + href = f'{appDomain}/share?act=Instapaper&u={user.name}&t={title}&k={share_key}&url={quote_plus(url)}' elif shareType == 'Weibo': href = f'https://service.weibo.com/share/share.php?url={quote_plus(url)}' - elif shareType == 'TencentWeibo': - href = f'https://share.v.t.qq.com/index.php?c=share&a=index&url={quote_plus(url)}' elif shareType == 'Facebook': href = f'https://www.facebook.com/share.php?u={quote_plus(url)}' elif shareType == 'X': - href = f'https://twitter.com/home?status={quote_plus(url)}' + href = f'https://twitter.com/intent/post?text={title}&url={quote_plus(url)}' elif shareType == 'Tumblr': href = f'https://www.tumblr.com/share/link?url={quote_plus(url)}' else: @@ -1153,7 +1151,7 @@ def download(self): Calling it more than once will lead to undefined behavior. :return: Path to index.html ''' - if 1: + try: res = self.build_index() if self.failed_downloads: self.log.warning(_('Failed to download the following articles:')) @@ -1171,8 +1169,8 @@ def download(self): self.log.warning(l) self.log.debug(tb) return res - #finally: - # self.cleanup() + finally: + self.cleanup() @property def lang_for_html(self): diff --git a/application/lib/calibre/web/fetch/simple.py b/application/lib/calibre/web/fetch/simple.py index fddbdcf3..66ff58e9 100644 --- a/application/lib/calibre/web/fetch/simple.py +++ b/application/lib/calibre/web/fetch/simple.py @@ -452,17 +452,17 @@ def process_images(self, soup, baseurl): tag['src'] = imgpath else: from calibre.utils.img import image_from_data, image_to_data - if 1: + try: # Ensure image is valid img = image_from_data(data) if itype not in {'png', 'jpg', 'jpeg'}: itype = 'png' if itype == 'gif' else 'jpeg' data = image_to_data(img, fmt=itype) if self.compress_news_images and itype in {'jpg','jpeg'}: - if 1: + try: data = self.rescale_image(data) - #except Exception: - # self.log.exception('failed to compress image '+iurl) + except Exception: + self.log.exception('failed to compress image '+iurl) # Moon+ apparently cannot handle .jpeg files if itype == 'jpeg': itype = 'jpg' @@ -471,9 +471,9 @@ def process_images(self, soup, baseurl): self.imagemap[iurl] = imgpath self.fs.write(imgpath, data, 'wb') tag['src'] = imgpath - #except Exception: - #traceback.print_exc() - #continue + except Exception: + traceback.print_exc() + continue #如果需要,纠正或规则化soup里面的图片地址,比如延迟加载等 def rectify_image_src(self, soup, baseurl=None): @@ -546,7 +546,7 @@ def process_links(self, soup, baseurl, recursion_level, into_dir='links'): self.fs.mkdir(diskpath) prev_dir = self.current_dir - if 1: + try: self.current_dir = diskpath tags = list(soup.find_all('a', href=True)) @@ -632,7 +632,7 @@ def process_links(self, soup, baseurl, recursion_level, into_dir='links'): finally: self.current_dir = diskpath self.files += 1 - #finally: + finally: self.current_dir = prev_dir if self.show_progress: print() diff --git a/application/lib/pocket.py b/application/lib/pocket.py index 76113a55..c945601b 100644 --- a/application/lib/pocket.py +++ b/application/lib/pocket.py @@ -60,7 +60,7 @@ def __str__(self): class Pocket(object): REQUEST_TOKEN_URL = 'https://getpocket.com/v3/oauth/request' - AUTH_TOKEN_URL = 'https://getpocket.com/auth/authorize?request_token=%(request_token)s&redirect_uri=%(redirect_uri)s' + AUTH_TOKEN_URL = 'https://getpocket.com/auth/authorize?request_token={}&redirect_uri={}' ACCESS_TOKEN_URL = 'https://getpocket.com/v3/oauth/authorize' POCKET_HEADERS = { @@ -76,9 +76,13 @@ def __init__(self, consumer_key, redirect_uri=None): def _post(self, method_url, **kw): ret = self.opener.open(method_url, data=kw) - if ret.status_code != 200: + if ret.status_code > 399: raise APIError(ret.status_code, ret.headers.get('X-Error-Code',''), ret.headers.get('X-Error',''), 'Get access token') - return json.loads(ret.content) + + try: + return json.loads(ret.content) + except: + return json.text def _authenticated_post(self, method_url, **kw): kw['consumer_key'] = self.consumer_key @@ -88,19 +92,18 @@ def _authenticated_post(self, method_url, **kw): def get_request_token(self): #此步仅用来直接通过一次http获取一个request_token(code),pocket不会回调redirect_uri ret = self._post(self.REQUEST_TOKEN_URL, consumer_key=self.consumer_key, redirect_uri=self.redirect_uri) - return ret.get('code', '') + return ret.get('code', '') if isinstance(ret, dict) else ret.split('=')[-1] def get_authorize_url(self, code): if not self.redirect_uri: raise APIError(400, '140', 'Missing redirect url.', 'Get access token') - url = self.AUTH_TOKEN_URL % {'request_token' : code, 'redirect_uri' : self.redirect_uri} - return url + return self.AUTH_TOKEN_URL.format(code, self.redirect_uri) def get_access_token(self, code): # access token : {"access_token":"dcba4321-dcba-4321-dcba-4321dc","username":"pocketuser"}. ret = self._post(self.ACCESS_TOKEN_URL, consumer_key=self.consumer_key, code=code) self.access_token = ret.get('access_token', '') - return ret + return self.access_token def set_access_token(self, access_token): self.access_token = str(access_token) diff --git a/application/lib/recipe_helper.py b/application/lib/recipe_helper.py index 22cd18f2..deed9bf7 100644 --- a/application/lib/recipe_helper.py +++ b/application/lib/recipe_helper.py @@ -49,9 +49,10 @@ class {classname}({base}): #能使用点号访问的字典 class DotDict(dict): + #__setattr__ = dict.__setitem__ + #__getattr__ = dict.__getitem__ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def __getattr__(self, key): try: return self[key] diff --git a/application/lib/smtp_mail.py b/application/lib/smtp_mail.py index f0cf9e91..0979ff23 100644 --- a/application/lib/smtp_mail.py +++ b/application/lib/smtp_mail.py @@ -32,10 +32,12 @@ def smtp_send_mail(sender, to, subject, body, host, username, password, port=Non part.add_header('Content-Disposition', f'attachment; filename="{filename}"') message.attach(part) - with smtplib.SMTP(host=host, port=port) as smtp_server: + klass = smtplib.SMTP if port != 465 else smtplib.SMTP_SSL + with klass(host=host, port=port) as smtp_server: smtp_server.connect(host, port) smtp_server.ehlo() smtp_server.starttls() smtp_server.ehlo() smtp_server.login(user=username, password=password) smtp_server.sendmail(sender, to, message.as_string()) + diff --git a/application/static/base.css b/application/static/base.css index c720311e..71dcb1d2 100644 --- a/application/static/base.css +++ b/application/static/base.css @@ -572,7 +572,7 @@ table.pure-table thead { color: #fff; text-align: center; padding: 3px 15px; - white-space: nowrap; + /*white-space: nowrap;*/ } .logs .status.success { @@ -880,3 +880,44 @@ div[class="schedule_daytimes"] input { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); z-index: 9999; } + +#up_cover_progress { + width:300px; + height:25px; + border:1px solid #98AFB7; + border-radius:5px; + margin:0 auto; +} +#up_cover_progress_bar { + width:0px; + height:25px; + border-radius:5px; + background:#5EC4EA; + text-align: center; +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; + cursor: help; +} +.tooltip .tooltiptext { + visibility: hidden; + width: 500px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 10px; + position: absolute; + z-index: 1; + top: calc(-100%); /*calc(10% + 5px);*/ + left: calc(50% - 500px); + opacity: 0; + transition: opacity 0.3s; +} +.tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +} \ No newline at end of file diff --git a/application/static/base.js b/application/static/base.js index c08350dd..662afc5e 100644 --- a/application/static/base.js +++ b/application/static/base.js @@ -1061,7 +1061,8 @@ function startUploadCoversToServer(url) { alert(i18n.imgSizeToLarge); return; } - + + $("#up_cover_progress").show(); $.ajax({ type: "post", url: url, @@ -1069,7 +1070,21 @@ function startUploadCoversToServer(url) { cache: false, contentType: false, processData: false, + xhr: function() { + var xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + var percent = Math.round((evt.loaded / evt.total) * 100); + $("#up_cover_progress_bar").css("width", String(percent) + "%"); + $("#up_cover_progress_bar").html(String(percent) + "%"); + } + }, false); + return xhr; + }, success: function(resp, status, xhr) { + $("#up_cover_progress").hide(); + $("#up_cover_progress_bar").css("width", "0px"); + $("#up_cover_progress_bar").html(''); if (resp.status == "ok") { ShowSimpleModalDialog('

' + i18n.uploadCoversOk + '

'); } else { @@ -1077,6 +1092,9 @@ function startUploadCoversToServer(url) { } }, error: function(xhr, status, error) { + $("#up_cover_progress").hide(); + $("#up_cover_progress_bar").css("width", "0px"); + $("#up_cover_progress_bar").html(''); alert(status); } }); diff --git a/application/static/faq.html b/application/static/faq.html deleted file mode 100644 index b2112995..00000000 --- a/application/static/faq.html +++ /dev/null @@ -1,448 +0,0 @@ - - - - - FAQ - KindleEar - - - - - - - - - -

KindleEar是什么?

-

KindleEar是开源免费网络应用,可以部署在大多数支持Python的托管平台,包括但不限于google appengine,Heroku,Pythonanywhere,主要功能是自动定期通过RSS收集网络文章然后制作成图文并茂的电子书推送至你的Kindle。

-

主要特性: -

- 优点: - -

- - -

此应用需要自己的服务器吗?

-

此应用托管在Google App Engine(GAE)服务器上,不需要你自己的服务器。

- 注意:GAE(Google App Engine)不是GCE(Google Computer Engine),只要应用使用的资源不超其免费额度,则GAE永久免费,但是GCE仅提供60天试用免费。 -

- - -

如何搭建自己的推送服务器?

-

-

    -
  1. Github 页面上下载KindleEar的最新版本,在页面的右下角有一个按钮"Download ZIP",点击即可下载一个包含全部源码的ZIP文档,然后解压到你喜欢的目录,比如C:\KindleEar(下面以这个为例)。
  2. -
  3. 首先你需要一个google账号,并暂时 [启用不够安全的应用的访问权限] 以便获得上传程序的权限。然后访问 Google Developers Console,新建一个Application。(用来上传KinldeEar用)。
  4. -
  5. 安装 Python 2.7.x,在此页面中选择你的系统平台对应的2.7.x版本下载,然后直接双击安装。注意不要安装成Python 3.x版本,KindleEar都暂不支持。
  6. -
  7. 下载 GAE SDK,选择273之前的版本,比如下面几个链接。 -
      -
    1. google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip
    2. -
    3. google-cloud-sdk-273.0.0-darwin-x86.tar.gz
    4. -
    5. google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz
    6. -
    7. google-cloud-sdk-273.0.0-linux-x86.tar.gz
    8. -
    9. google-cloud-sdk-273.0.0-linux-x86_64.tar.gz
    10. -
    11. google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip
    12. -
    13. google-cloud-sdk-273.0.0-windows-x86.zip
    14. -
    15. google-cloud-sdk-273.0.0-windows-x86_64.zip
    16. -
    -
  8. -
  9. 在app.yaml的同一个目录,打开config.py文件,修改SRC_EMAIL为你的gmail账号,DOMAIN为你的应用的域名。其他的选项也可以根据注释有选择性的修改,或者使用默认即可。
  10. -
  11. 正式上传KindleEar。 -
      -
    1. 依次点击:开始-运行-输入CMD,打开命令行窗口,进入KindleEar目录
    2. -
    3. gcloud auth login
    4. -
    5. gcloud config set project 你的ApplicationId
    6. -
    7. gcloud app deploy --version=1 app.yaml module-worker.yaml
    8. -
    9. gcloud app deploy --version=1 KindleEar目录
    10. -
    11. 如果出现错误,可能需要下面几条命令。 -
        -
      1. gcloud datastore indexes create index.yaml
      2. -
      3. gcloud app deploy --version=1 app.yaml queue.yaml
      4. -
      5. gcloud app deploy --version=1 app.yaml cron.yaml
      6. -
      7. gcloud app deploy --version=1 app.yaml dispatch.yaml
      8. -
      -
    12. -
    -
  12. -
  13. 显示上传成功后使用你的浏览器打开域名:kindleear123.appspot.com (举例而已,kindleear123要改为你申请的GAE程序名称),如果能正常打开,则还有最后一步。
  14. -
  15. 将你的gmail账号加入kindle推送邮箱的白名单。这一步你应该早就会了吧。
  16. -
  17. 然后呢?享受你自己的推送服务吧!
  18. -
-

- - -

为什么我收不到推送的RSS?

-

收不到推送的RSS原因很多,如下是几种可能: -

    -
  1. 部署时仅部署了default模块,而没有部署worker模块。比如使用Launcher默认配置上传则导致此情况。
  2. -
  3. 没有打开"自定义RSS"的投递开关,此开关在设置网页的最下面。
  4. -
  5. 没有在kindle邮箱的白名单中加入你的gmail地址:
  6. - -
  7. 添加RSS过多导致内存占用过大,程序被自动终止。 -
    • 打开module-worker.yaml,将instance_class值修改为B4,然后重新上传。
    -
  8. -
  9. 你输入的订阅地址不是合法的RSS/ATOM订阅(XML格式)。
  10. -
  11. 如果RSS订阅源是Feedburner生成的,你可以在其链接之后添加一个查询字符串'?format=xml'或'?fmt=xml',比如:http://xx.com/feed?format=xml.
  12. -

- - -

何为全文RSS?

-

全文RSS就是在RSS的XML文件中已经给出了文章全文的RSS,使用浏览器打开RSS对应的链接,查看是否已经有全部的文章内容?如果是,则为全文RSS。如果仅给出文章摘要,则不是。

- - -

全文RSS能否按照摘要RSS处理?反之是否可以?

-

全文RSS当然可以按照摘要RSS处理,这样就忽略RSS链接中给出的文章内容,而直接到原链接中获取,只是时间要多花费不少,导致支持的RSS数量下降。如果是摘要RSS,则不能按全文RSS处理,否则会导致文章内容不全。

- - -

如何按周订阅?

-

有些RSS每周更新,这样每天推送同样的内容就蛋疼了点,碰到这种情况,有两种处理方法: -

    -
  1. 在books目录下添加一个py文件,将参数deliver_days设置为你需要推送的星期(头字母大写的英文星期单词),注意deliver_days是一个列表,你也可以设置为几个单词,这样的话,其实也可以设置为哪天不推送。 -
    比如:
    deliver_days=['Friday']
    deliver_days=['Sunday', 'Friday']
  2. -
  3. 使用管理员登陆,新增一个账号,退出,使用新账号登陆,添加需要的RSS,然后在“设置”页面仅选择你需要的其中一个“投递日”,在页面下方的“最旧文章”中选择“一个星期”。

- - -

如何一天推送多次?

-

需要及时跟踪网站最新消息的同学可能有一天推送多次的需求,以下步骤告诉你如何实现: -

    -
  1. 在books目录下添加一个py文件。
  2. -
  3. deliver_times设置为你需要推送的整点时间列表。 -
    比如:
    deliver_times=[6,14,22] #6:00,14:00,22:00三次推送 -
  4. -
  5. 如果需要,oldest_article设置为多次推送的间隔秒数,这样就不推送重复文章了。 -
    比如:
    oldest_article=28800 #8*60*60 -
  6. -
-

- - -

URL过滤器功能如何使用?

-

URL过滤器为高级功能,面向懂python正则表达式的同学,应用场景是这样的:有一些RSS的一部分固定的图像会一直下载不了或者其本身就是烦人的广告图像,为了节省时间支持更多的RSS或更好的阅读体验,则可以过滤这些图像URL,不再下载。URL过滤器支持正则表达式,请严格按python正则表达式的语法编写URL过滤器。

- - -

部分RSS有敏感内容,被墙屏蔽了怎么办?

-

升级到1.5及以上版本,并且将被qiang的RSS的链接协议改为https。

- - -

自定义书籍py文件,如何将封面/报头放到py同一目录?

-

升级到1.5及以上版本,在封面/报头文件设置中有全路径信息即可,如下:
- mastheadfile = 'books/xxxx.gif'
- coverfile = 'books/xxxx.jpg'

- - -

GAE上传遇到SSL出错,如何解决?

-

墙内的朋友有时候在上传代码时遇到此错误:urllib2.URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in violation of protocol>,如果这样,改hosts即可:“74.125.229.174 appengine.google.com”

- - -

有部分文章乱码?

-

对于自定义RSS和不指定编码的内置书籍,软件使用多种方法综合确定网页的编码:HTTP头/HTML头/python模块chardet检测的编码,最终结果并不是100%正确的,如果检测错误,则文章可能乱码,这在浏览器上偶尔碰到网页乱码一样道理。 -
因为这种情况很少见,KindleEar仅提供一个不是很优雅的hack方案:手工更改数据库缓存的chardet检测的编码。
方法如下: -

  1. 使用admin登陆appid.appspot.com(假定你的应用名字为appid)。
  2. -
  3. 浏览器地址栏中输入https://appid.appspot.com/dbviewer,翻到页面UrlEncoding区段,查看乱码文章对应网站的编码信息,如果编码不对,记下ID号,进行下一步。
  4. -
  5. 地址栏中输入https://appid.appspot.com/dbviewer?action=modurlenc&id=dbid&feedenc=fenc&pageenc=penc -
      解释一下,在这一长串字符串中,你先要更改: -
    • dbid:你记下来的编码对应网站ID。
    • -
    • fenc:你希望更改FeedEncoding的编码。
    • -
    • penc:你希望更改PageEncoding的编码。
    • -
  6. -
  7. 或者你可以在GAE后台的Datastore Viewer中修改。
  8. -
  9. 至于怎么获取正确的编码,可以通过在浏览器中查看网页源码。
  10. -
  11. 如果你在dbviewer页面中找不到你需要修改的网站,则说明KindleEar一直没有使用chardet检测过此网站任何网页编码,仅使用HTTP头/HTML头信息,这种情况你可以设置config.py文件中的ALWAYS_CHAR_DETECT变量为True再尝试。
  12. -
  13. 如果设置ALWAYS_CHAR_DETECT为True还是乱码,则说明chardet检测到的编码错误,并且HTTP头/HTML头编码信息不一致(还是有这种奇葩的网站的),这种情况还可以尝试将TRUST_ENCODING_IN_HEADER_OR_META设置为True。
  14. -
  15. 尝试了ALWAYS_CHAR_DETECT和TRUST_ENCODING_IN_HEADER_OR_META都不行的话,软件无能为力了,请增加一个py文件,然后手工指定正确的编码吧。
  16. -
-

- - -

忘记密码了怎么办?

-

如果是忘记非管理员账号的密码,则可以通知管理员改密码。
如果是管理员密码,则可以登陆 App Engine,在左边选择 "Datastore | 实体",在种类里面选择 "KeUser",选中 name 为 admin 的行,记下 "secret_key" 的值,比如为 "abcdefgh",在前面加随便几个字符,比如 "123abcdefgh",然后随便搜索一个在线的MD5计算网站,将其转换为MD5摘要字符串,比如 "8b920d97ca5fceff6c5835223c541bf4",再点击修改上面选择的数据行,将最后的MD5字符串填写到passwd域,然后点击页面下方的保存,重新使用密码 "123" 登录。 - - -

xxx@appid.appspotmail.com邮件地址怎么用?

-

xxx@appid.appspotmail.com是KindleEar附送的邮件服务,在部署好KindleEar后,你自动拥有了无数个EMAIL邮箱地址,格式为:xxx@appid.appspotmail.com,xxx为任意合法字符串,appid为你的应用名。
-

-

- - -

推送至亚马逊中国邮箱有比较大的延迟?

-

这个经过网友们试验,和伟大的墙有关系,有一个解决方法:KindleEar先推送至某个个人邮箱,然后此个人邮箱自动转发至中亚的kindle邮箱。
不过还面临一个问题:部分邮箱设置自动转发需要发到邮箱的验证,而亚马逊邮箱不能登陆无法验证。其实你需要的是找一个不需要目标邮箱确认的邮箱来设置自动转发,比如outlook.com。

- - -

为何合并多本书籍推送后无法查词典?

-

如果有外文RSS而且需要在kindle上查字典的话,请将自定义RSS的语言设置为对应的外文。
- 同时,还要注意合并书籍推送的其他特性:
-

    -
  1. 合并后的书籍使用默认报头和封面;
  2. -
  3. 合并后的书籍使用自定义RSS设定的标题和语言设置;
  4. -
-

- - -

“高级设置”里的归档和分享功能如何使用?

-

启用此功能后,KindleEar会在每篇文件的最后都附加一个超链接,在Kindle上点击此超链接会自动打开浏览器将你正在阅读的此文章归档至网络笔记或分享至社交网络(需要打开网络连接)。
- 有几点需要注意的: -

  1. 请先修改config.py里面的DOMAIN为你申请的应用域名;
  2. -
  3. 如果你在红墙内,可能需要打开“我需要翻墙(针对中国用户)”开关,默认的翻墙转发器采用作者在heroku上搭建的免费服务器: http://kforwarder.herokuapp.com,如果你有余力或者注重隐私,建议自己搭建一个,源码托管在 https://github.com/cdhigh/forwarder。(如果将来墙也封锁了heroku,则此翻墙方法无效)
-

- - -

有的网站需要登陆才能阅读文章的问题如何解决?

-

- 有些网站需要先注册账户然后登陆后才能阅读和下载文章,对于此类网站,则可以仿照books目录下的预置书籍,增加一个新的py文件,然后设置这几个属性: -

    -
  1. needs_subscription :设置为True
  2. -
  3. login_url :为此网站的登陆网页URL
  4. -
  5. form_4_login :可选(如果你不会看网页HTML源码就暂时不用设置),如果不设置或设置为None则KindleEar尝试自动判断;设置为整数则表示此网页中第几个form为登陆表单(0为第一个);设置为字符串则表示对应登陆表单form的名字,'#'打头表示id,'.'打头表示class,否则则匹配name/id/class任何一个。
  6. -
  7. 登陆KindleEar后,在“我的订阅”页面先订阅对应书籍,然后书籍行右边会新出现一个按钮“登陆信息”, 点击后请在新页面中输入正确的账号和密码。
  8. -
-

注1:因为GAE环境的限制,需要执行javascript或要输入验证码的网站则无法支持。

-

注2:对于一些足够特殊和复杂的网站,可能你需要在书籍子类中重写login()函数。

-

注3:你输入的密码将加密保存,密钥为每个账号都不一样的8位随机字符串,有一定的安全性,而且我尽量照顾你的密码安全,你可以随时删除保存的密码信息,书籍退订后也马上删除密码,但因为密钥也保存在GAE上(在另外的数据表),所以不能保证很高的安全性,请自己明白并愿意承担其中的风险。

-

- - -

网站在IE6下显示不正常?

-

在IE6下网站排版混乱并且无法显示导航栏,因为网站的排版采用YAHOO的CSS框架pure,而此框架要求IE7以上的IE,或者chrome/firefox,如果你使用的IE版本过低,建议升级到新版本。

- - -

内置书籍订阅时“Separate”选项是什么意思?

-

一旦你选择了“合并推送”,则所有订阅源都会合并成一本书籍推送至kindle,如果你需要有部分订阅源分开单独推送,则在订阅时选择“Separate“。

- - -

部署应用后打开网页出现“internal server error”如何处理?

-

- 有部分网友在部署后打开此应用的“我的订阅”和“投递日志”网页会出现“internal server error”错误,一般情况下因为gae没有及时生成对应数据库的索引导致。
你可以打开gae后台,查看logs日志,看是否有“NeedIndexError: no matching index found.”异常,如果有的话,证明是没有生成索引。
你也可以在gae后台查看“Datastore Indexes”,看每个条目的“Status”,正常情况下应该都是“Serving”,如果是“Building”或其他值,则说明索引也没有正确生成。
- 如果你部署步骤完全正确的话,可以稍等几分钟到几十分钟,等gae自动生成索引,如果gae没有成功自动生成索引,你也可以手动更新索引:
- python appcfg.py update_indexes KindleEar/
- 在终端窗口执行此命令,注意最后一个KindleEar是你保存的此应用的目录名。
- 如果出现的异常信息中有“UnknownQueueError”,则说明部署有问题,可以先单独执行此命令上传更新队列配置信息:
- python appcfg.py update_queues KindleEar/
- 这几个命令也可以执行一遍试试。(比如手动可以推送无法自动推送则执行 update_cron)
- - python appcfg.py update_cron KindleEar/
- python appcfg.py update_queues KindleEar/
- python appcfg.py update_dispatch KindleEar/ -

- 如果上面的步骤执行完后还是不行的话,请按照正确步骤重新部署。 -

- - -

如何修改多本书籍合并后的封面?

-

默认配置下如果合并多本书籍,则合并后的封面会将所有书籍的封面缩小并拼贴在一起,并且每次推送的排列顺序都不一样,此效果由config.py里面的参数项 DEFAULT_COVER_BV 控制,默认为 None。
- 如果你不喜欢这个效果,设置DEFAULT_COVER_BV为某个文件名则可以取消此效果,比如: - DEFAULT_COVER_BV='cv_bound.jpg' -

- - -

如何订阅微信公众号?

-

网站 今天看啥 提供微信公众号转RSS服务(注意在添加RSS时选择isfulltext选项)。 -

- - -

有关墙的汇总

-

在天朝的网友在部署时和使用时会遇到一个强大的墙,特别google又是特别重要的黑名单,这是几个简单的说明,其他的请自己解决: -

-

- - -

保存到Pocket功能如何使用?

-

Pocket 是一个比较流行的稍后阅读应用,你在电子书上读到有趣或有用的文章后,可以选择将其发送到你的Pocket账号,然后就可以通过跨平台的Pocket应用来继续阅读。
要启动此功能,请按如下步骤打开: -

- 备注: - -

- - -

投递日志状态wrong SRC_EMAIL的解决方案。

-

到Gae后台的Settings页面,看看 已经授权的Email列表里面有没有你的发送邮箱地址,如果没有就添加即可。 -

- - -

其他的一些小Tips。

-

-

-

- - -

漫画订阅说明

-

- 目前支持以下网站的漫画
-

- 漫画的订阅方法有两种,可以选择其中任意一种。
- 这里使用【食戟之灵】举例,先从动漫狂中搜索得到该漫画的URL。
- -

- -

使用sendgrid发送邮件

-

- 在设置界面,可以设置sendgrid发送邮件,需要在下面网站申请帐号并创建自己的apikey。
- sendgrid
- 推荐按照Google的官方说明进行操作。 - google
- 需要说明的是sendgrid支持的邮件大小是30M,但是由于gcloud的urlfetch的限制变成了10M。 -

- -

我还有更多问题,到哪里去问?

-

如果你碰到更多问题,可以到 https://github.com/cdhigh/KindleEar/issues 去提交一个issue,然后等待答复,不过在提交问题之前,还是建议先搜索一下别人先前提交的issues,说不定已经有人重复提过了相关问题呢?
- 如果没有别人提过类似问题,你在提交新问题的时候,建议附上 GAE后台 的Logs信息,以便定位问题,也可以得到更快的答复。 -

- - - - diff --git a/application/static/faq_en.html b/application/static/faq_en.html deleted file mode 100644 index 88d863b2..00000000 --- a/application/static/faq_en.html +++ /dev/null @@ -1,309 +0,0 @@ - - - - - FAQ - KindleEar - - - - - - - - - -

What is KindleEar?

-

KindleEar is a free webapp that running on Google App Engine, it aggregates feeds and deliver the generated docs to your Kindle periodically.

-

Features: -

- Benefits: - -

- - -

Dows this application require its own server?

-

This application is hosted on Google App Engine (GAE), GAE has free quotas enough for small applications like KindleEar, you don't need to pay a penny to run KindleEar in GAE, it's fine if you don't own a server(VPS or other infrastructure).

- Remember: GAE(Google App Engine) is not GCE (Google Computer Engine), GAE is free forever (with limit of quotas of resources), GCE is free only within 60 days trial. you don't need to start a GCE trial. -

- - -

How to deploy my own delivery server?

-

-

    -
  1. Download the latest version of KindleEar on github and then extract it to somewhere, for example C:\KindleEar (we'll use it in the following steps).
  2. -
  3. You need a google account, please visit the Google Developers Console page. Use your gmail account to login, click on "Create an empty Project", follow steps to create a GAE application.
  4. -
  5. Install Python 2.7.x. Download the latest 2.7.x source release and then install it. Do not install a Python 3.x version, because KindleEar does not support it now.
  6. -
  7. Install GAE SDK, choose some version before 273. for example: -
      -
    1. google-cloud-sdk-273.0.0-windows-x86_64-bundled-python.zip
    2. -
    3. google-cloud-sdk-273.0.0-darwin-x86.tar.gz
    4. -
    5. google-cloud-sdk-273.0.0-darwin-x86_64.tar.gz
    6. -
    7. google-cloud-sdk-273.0.0-linux-x86.tar.gz
    8. -
    9. google-cloud-sdk-273.0.0-linux-x86_64.tar.gz
    10. -
    11. google-cloud-sdk-273.0.0-windows-x86-bundled-python.zip
    12. -
    13. google-cloud-sdk-273.0.0-windows-x86.zip
    14. -
    15. google-cloud-sdk-273.0.0-windows-x86_64.zip
    16. -
    -
  8. -
  9. Modify variables 'SRC_EMAIL' and 'DOMAIN' and 'TIMEZONE' in config.py in same directory of app.yaml. You can modify other variables if you want and understand what it means.Using the default values are recommended.
  10. -
  11. Start upload KindleEar now. -
      -
    • Followed by clicking: Start - Run - type cmd, it will open a command line window
    • -
    • Change current directory to App Engine SDK installation directory, example: cd C:\Program Files\Google\google_appengine
    • -
    • gcloud auth login
    • -
    • gcloud config set project YourAppId
    • -
    • gcloud app deploy --version=1 app.yaml module-worker.yaml
    • -
    • gcloud app deploy --version=1 KindleEarDirectory
    • -
    • If something goes wrong, execute following command lines. -
        -
      1. gcloud datastore indexes create index.yaml
      2. -
      3. gcloud app deploy --version=1 app.yaml queue.yaml
      4. -
      5. gcloud app deploy --version=1 app.yaml cron.yaml
      6. -
      7. gcloud app deploy --version=1 app.yaml dispatch.yaml
      8. -
      -
    • -
    -
  12. -
  13. After successfully uploading KindleEar, use your browser to open the site: http://kindleear123.appspot.com (example only, kindleear123 should be changed to the name of your GAE application). If everything goes fine, then go to the last step.
  14. -
  15. Add your gmail account to your kindle email whitelist. You should know how to do it already.
  16. -
  17. Then what? Enjoy your own delivery service now!
  18. -
  19. Tips: the initial username of application is admin, default password is admin too.
  20. -
-

The easy way(recommended): -

    -
  1. Download KindleEar and extract it, get a GAE account, create an application.
  2. -
  3. Download uploader. (for windows user only)
  4. -
  5. Extract 'uploader' to a directory, move the folder of KindleEar into the folder of 'uploader' and rename it to kindleear, then double-click 'uploader.bat' to upload.
  6. -
  7. Tips: the initial username of application is admin, default password is admin too.
  8. -

-

- - -

Why do I not receive the deliveries?

-

There are many possible reasons: -

    -
  1. You haven't enabled the "Enable deliver custom rss" option, this option is at the bottom of the setting page.
  2. -
  3. You haven't added your gmail address to Amazon's 'Approved Personal Document E-mail List'.
  4. -
  5. The content of the rss link isn't a valid XML format.
  6. -
  7. For those feeds generated by Feedburner, you can append a query string '?format=xml' to end of url, for example: http://xx.com/feed?format=xml.
  8. -

- - -

What is full text rss?

-

Full text rss means that it contains all the contents of the articles in the rss, while summary rss contains only brief descriptions of the articles. You can check it by opening the link of the rss.

- - -

Can full text rss be processed as summary rss? What about the reverse?

-

Full text rss can be treated as summary rss. In this case, program will ignore the content in the rss xml file and fetch full text from the original link. It costs much time which will result the decreasing of supported rss quantities. If summary rss is processed as full text rss, articles sent to your kindle would still have brief descriptions only.

- - -

How to deal with weekly subscription?

-

Some rss are updated weekly. For this type of rss, you can touch a new python file in the 'books' directory with variable 'deliver_days' setted to a day.
- deliver_days=['Friday']

- - -

Can KindleEar deliver news to my kindle more than once a day?

-

The answer is yes, follow the next steps to make it come true: -

    -
  1. Add a python file to the directory of 'books'.
  2. -
  3. Set 'deliver_times' using a list in the new python file. -
    For example:
    deliver_times=[6,14,22] #6:00,14:00,22:00 deliver three times a day -
  4. -
  5. Optional: set 'oldest_article' to the number of interval seconds between two deliveries. -
    For example:
    oldest_article=28800 #8*60*60 -
  6. -
-

- - -

How to use the url filter?

-

Url filter is not for beginners, you need to know regular expression and python. Urls in the list would not be downloaded for saving time.

- - -

What can I do if the gfw of China blocked some rss?

-

Change the link to use https protocol.

- - -

How to put the cover or the masthead images in the same directory when I add a customize book?

-

Set the variable mastheadfile and coverfile to a path, for example:
- mastheadfile = 'books/xxxx.gif'
- coverfile = 'books/xxxx.jpg'
-

- - -

How to deal with the SSL error problem when I upload KindleEar?

-

Some friends in China would encounter "urllib2.URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in violation of protocol>". To solve this problem, try to add a new line "74.125.229.174 appengine.google.com" to the hosts file.

- - -

Why some articles have garbled characters?

-

For custom rss and the built-in books without encoding specified, program detects encoding automatically by using http header, html meta or the chardet module of python. Howerver, the result is not 100% accurate. If the encoding detecting is wrong, the article may be garbled. Because this situation is very rare, KindleEar only supplies an not very elegant solution: manually change the chardet-detect encoding saved in the database. Steps as follows:
-

  1. Login appid.appspot.com by using account 'admin'(assuming your application name is appid).
  2. -
  3. Visit 'https://appid.appspot.com/dbviewer'. Check the encoding of the website whose articles are garbled in the UrlEncoding section. If the encoding is wrong, take down its ID.
  4. -
  5. Now visit 'https://appid.appspot.com/dbviewer?action=modurlenc&id=dbid&feedenc=fenc&pageenc=penc' -
      Before you press enter, modify some vaiables: -
    • dbid: the ID you just took down.
    • -
    • fenc: new correct FeedEncoding.
    • -
    • penc: new correct PageEncoding.
    • -
  6. -
  7. Or you can change them in 'Datastore viewer' in the GAE backend.
  8. -
  9. You can get the correct encoding by reading source code of a webpage.
  10. -
  11. If you cannot find the website in page 'dbviewer', it means KindleEar has never detected the encoding of any page of this website, http header or html meta is used only. In this case, you can set the variable 'ALWAYS_CHAR_DETECT' in 'config.py' to True.
  12. -
  13. If the article still has garbled characters after setting 'ALWAYS_CHAR_DETECT' to True, it means that encoding detected by chardet is wrong and doesn't match with the http header or the html meta. The final attempt is to set 'TRUST_ENCODING_IN_HEADER_OR_META' to True.
  14. -
  15. With all the solutions tested and the problem still exists, please add the rss to a python file in the folder 'books' with encoding specified.
  16. -
-

- - -

What can I do if I forgot the password?

-

If you are an non-administrator user, you can ask the administrator to change your password.
If forgot the administrator's password, you can login App Engine, select "Datastore | Entries" on the left menu, select "KeUser" in the "Kind" Combobox, write down the value of field "secret_key" of database line "admin", let's assume that is "abcdefgh", add some characters at the beginning, become "123abcdefgh", find a website to calculate the MD5 hash of string "123abcdefgh", will be "8b920d97ca5fceff6c5835223c541bf4", fill it in field "passwd" of database line "admin". and then use admin/123 to login. - - -

How to use mail service xxx@appid.appspotmail.com?

-

- You'll have uncountable mail addresses like xxx@appid.appspotmail.com as long as xxx is valid ascii string after you have deployed KindleEar(appid is your application id).
-

-

- - -

Why I can't lookup words in the delivery books after I chosen 'Merge books into one'? 

-

With the option 'Merge books in one' checked, KindleEar will set the metadata of the book as Custom Rss. To fix this problem, you have to set the language attribute in the 'Custom Rss Setting' section manually.

- - - -

How to deal with those websites that need login before I can view the articles?

-

- If you want to crawl a website that needs login, you have to add a python file to the folder 'books'. Refer to sample books if you don't know how to write it. -

    -
  1. needs_subscription : Set to True
  2. -
  3. login_url : The URL for login page
  4. -
  5. form_4_login : Optional, if not set, KindleEar will guess which form in the page is used to login. Set to an integer indicate the sequence number (where 0 is the first) of the form in the page. Set to a string indicate the name of the form, you can use CSS selector: '#' for id and '.' for class, string only will match any of it(name/id/class).
  6. -
  7. Then login to KindleEar. In the 'Feeds' page, a new 'Login info' button will appear on the right side of the book which needs_subscription setted to True in the 'Subscripted' section. Click on it and then fill in your account name and password.
  8. -
-

Note1: currently we don't support those sites that need run javascript to login or need to input a captcha.

-

Note2: for some special site, you may have to overwirte the 'login' function which inherits from the 'BaseFeedBook' class.

-

Note3: the password will be stored encrypted on GAE, the key for encryption is a random string for each account of KindleEar, but the key is also stored on GAE, so it's not absolutely safe, please keep this in mind.

-

- - -

Can I delete/hide books in Chinese?

-

- If you are not Chinese, you can delete some *.py files in directory 'books'.
'__init__.py' and 'base.py' are required, please don't remove them.
- If you deploy the application via uploader, uploader can help you to delete books which language is Chinese. -

- - -

What's the meaning of the "Separate" option for the built-in books?

-

If you choose this option, the book you chosen will be sent to kindle separately even if the option "Merge books into one" is chosen.

- - -

How to solve it if website shows "internal server error" exception.

-

- Open appengine.google.com, choose your application, open "Logs", if have “NeedIndexError: no matching index found.” exception, then you can execute command in your computer.
- python appcfg.py update_indexes YourKindleEarFolder/
- if have “UnknownQueueError” exception, execute:
- python appcfg.py update_queues YourKindleEarFolder/
- - After command be executed, please wait some miniutes for GAE generate index for you. - If the problem still exist, you should deploy KindleEar again. -

- - -

How to use the feature 'Save to Pocket'?

-

The Pocket is a widely used 'Read It Later' service that enables users to save articles or web pages to the cloud for future reading. These articles are then synced to the user's Pocket list, accessible across all their devices, facilitating offline reading. Pocket streamlines articles by removing clutter and provides customizable text settings for a more comfortable reading experience.Now, you have the option to save a Kindle article to your Pocket account, allowing you to read it on other devices at your convenience.
- Follows steps to enjoy this feature: -

- - Remarks: - - -

- - -

My logs shows error 'wrong SRC_EMAIL'?

-

- Go to your GAE console, in 'Settings' | 'Application settings' page, add your email into field 'Email API authorized senders'. -

- - - - diff --git a/application/static/periodical.gif b/application/static/periodical.gif deleted file mode 100644 index a3c57354..00000000 Binary files a/application/static/periodical.gif and /dev/null differ diff --git a/application/static/separate.gif b/application/static/separate.gif deleted file mode 100644 index 6edc7dbe..00000000 Binary files a/application/static/separate.gif and /dev/null differ diff --git a/application/templates/adv_archive.html b/application/templates/adv_archive.html index 223371cb..05994172 100644 --- a/application/templates/adv_archive.html +++ b/application/templates/adv_archive.html @@ -18,7 +18,7 @@ {% if evernote.get('enable') %} checked="1" {% endif %} /> {{appendStrs['Evernote']}} - +
@@ -26,7 +26,7 @@ {{appendStrs["Wiz"]}} - +

@@ -78,10 +78,6 @@ {{appendStrs["Weibo"]}} -