├── .idea ├── NovelReader.iml ├── encodings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── app ├── __init__.py ├── forms.py ├── models.py ├── routes.py ├── static │ └── read.ico ├── tasks.py └── templates │ ├── _book.html │ ├── _book_list.html │ ├── _book_list_book.html │ ├── _user.html │ ├── author.html │ ├── background.html │ ├── base.html │ ├── book_detail.html │ ├── book_list_detail.html │ ├── book_list_rank.html │ ├── chapter.html │ ├── classify.html │ ├── download_list.html │ ├── index.html │ ├── login.html │ ├── permission_denied.html │ ├── rank.html │ ├── read.html │ ├── read_setting.html │ ├── register.html │ ├── search_result.html │ ├── source.html │ ├── source_not_found.html │ ├── user_detail.html │ ├── user_list.html │ └── view_documents.html ├── config.py ├── create_db.py ├── debug.py ├── debug_server.py ├── favicon.ico ├── lightreader.py └── requirements.txt /.idea/NovelReader.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightReader 2 | ## 简阅小说网 3 | 4 | 使用了追书神器API 5 | 6 | 可以订阅、换源 7 | 8 | 后台任务依赖RQ和Redis 9 | 10 | 需要先安装Redis: 11 | 12 | sudo apt install redis-server 13 | 14 | 然后运行: 15 | 16 | rq worker lightreader-tasks 17 | 18 | 因追书神器停更所有起点系小说,简阅受到波及,暂时停止更新和维护,恢复时间未知 19 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from flask_login import LoginManager 3 | from flask_sqlalchemy import SQLAlchemy, SignallingSession 4 | from flask_migrate import Migrate 5 | from flask_bootstrap import Bootstrap 6 | from config import Config 7 | from flask_uploads import UploadSet, configure_uploads, TEXT 8 | from flask_moment import Moment 9 | from flask_babel import Babel 10 | from redis import Redis 11 | import rq 12 | 13 | 14 | # class MySQLAlchemy(SQLAlchemy): 15 | # def create_session(self, options): 16 | # options['autoflush'] = False 17 | # return SignallingSession(self, **options) 18 | 19 | 20 | app = Flask(__name__) 21 | app.config.from_object(Config) 22 | login = LoginManager(app) 23 | db = SQLAlchemy(app) 24 | migrate = Migrate(app=app, db=db) 25 | bootstrap = Bootstrap(app) 26 | login.login_view = 'login' 27 | text = UploadSet("downloads", TEXT) 28 | configure_uploads(app, text) 29 | moment = Moment(app) 30 | babel = Babel(app) 31 | redis = Redis.from_url(app.config['REDIS_URL']) 32 | app.task_queue = rq.Queue('lightreader-tasks', connection=redis) 33 | 34 | from app import models, routes 35 | 36 | import create_db 37 | 38 | 39 | @babel.localeselector 40 | def get_locale(): 41 | return request.accept_languages.best_match(app.config['LANGUAGES']) 42 | -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, SubmitField, BooleanField, IntegerField 3 | from wtforms.validators import Email, DataRequired, ValidationError, EqualTo 4 | from app.models import User 5 | 6 | 7 | class LoginForm(FlaskForm): 8 | username = StringField('用户名', validators=[DataRequired()]) 9 | password = PasswordField('密码', validators=[DataRequired()]) 10 | remember_me = BooleanField('记住我') 11 | submit = SubmitField('登录') 12 | 13 | 14 | class RegistrationForm(FlaskForm): 15 | username = StringField('用户名', validators=[DataRequired()]) 16 | # email = StringField('邮箱地址', validators=[DataRequired(), Email()]) 17 | password = PasswordField('密码', validators=[DataRequired()]) 18 | password2 = PasswordField('重复密码', validators=[DataRequired(), EqualTo('password')]) 19 | submit = SubmitField('注册') 20 | 21 | def validate_username(self, username): 22 | user = User.query.filter_by(name=username.data).first() 23 | if user is not None: 24 | raise ValidationError('请输入一个不同的用户名') 25 | 26 | # def validate_email(self, email): 27 | # user = User.query.filter_by(email=email.data).first() 28 | # if user is not None: 29 | # raise ValidationError('请输入一个不同的邮箱') 30 | 31 | 32 | # class ResetPasswordRequestForm(FlaskForm): 33 | # email = StringField('Email', validators=[DataRequired(), Email()]) 34 | # submit = SubmitField('Request Password Reset') 35 | # 36 | # 37 | # class ResetPasswordForm(FlaskForm): 38 | # password = PasswordField('password', validators=[DataRequired()]) 39 | # password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')]) 40 | # submit = SubmitField('Request Password Reset') 41 | 42 | 43 | class SearchForm(FlaskForm): 44 | search = StringField('搜索', validators=[DataRequired()]) 45 | submit = SubmitField('搜索') 46 | 47 | 48 | class JumpForm(FlaskForm): 49 | page = IntegerField('页码', validators=[DataRequired()]) 50 | submit = SubmitField('跳转') 51 | -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from app import db, login 2 | import app 3 | from flask_login import UserMixin, current_user 4 | from datetime import datetime 5 | from werkzeug.security import check_password_hash, generate_password_hash 6 | import rq, redis 7 | from flask import current_app 8 | 9 | 10 | # subscribe = db.Table('subscribe', 11 | # db.Column('user_id',db.Integer,db.ForeignKey('user.id')), 12 | # db.Column('book_id',db.Integer,db.ForeignKey('book.id')) 13 | # ) 14 | 15 | 16 | class User(UserMixin, db.Model): 17 | __tablename__ = 'user' 18 | id = db.Column(db.Integer, primary_key=True) 19 | name = db.Column(db.String(20), unique=True, nullable=False) 20 | # email = db.Column(db.String(50),unique=True,nullable=False) 21 | password_hash = db.Column(db.String(128)) 22 | can_download = db.Column(db.Boolean) # 表示用户是否有下载权限,0表示没有,1表示有 23 | last_seen = db.Column(db.DateTime, default=datetime.now()) 24 | user_ip = db.Column(db.String(20)) 25 | user_agent = db.Column(db.String(256)) 26 | is_admin = db.Column(db.Boolean) # 表示用户是否是管理员,0表示不是,1表示是 27 | font_size = db.Column(db.String(10)) # 用户设置的字体大小 28 | night_mode = db.Column(db.Boolean, default=0) # 夜间模式,0表示关,1表示开 29 | 30 | # subscribing = db.relationship('Book', 31 | # secondary=subscribe, 32 | # primaryjoin=(subscribe.c.user_id==id), 33 | # backref=db.backref('subscribe',lazy='dynamic'), 34 | # lazy='dynamic') 35 | 36 | tasks = db.relationship('Task', backref='user', lazy='dynamic') 37 | 38 | def __repr__(self): 39 | return '' % self.name 40 | 41 | def set_password(self, password): 42 | self.password_hash = generate_password_hash(password) 43 | 44 | def check_password(self, password): 45 | return check_password_hash(self.password_hash, password) 46 | 47 | # # 以下几个方法存在不明bug 48 | # def is_subscribing(self,book): 49 | # return self.subscribing.filter(subscribe.c.book_id == book.id).count() > 0 50 | # 51 | # def subscribe(self,book): 52 | # if not self.is_subscribing(book): 53 | # self.subscribing.append(book) 54 | # 55 | # def un_subscribe(self,book): 56 | # if self.is_subscribing(book): 57 | # self.subscribing.remove(book) 58 | 59 | def launch_task(self, name, description, source_id, book_id): 60 | rq_job = current_app.task_queue.enqueue('app.tasks.' + name, current_user.id, source_id, book_id) 61 | task = Task(id=rq_job.get_id(), name=name, description=description, user=self) 62 | db.session.add(task) 63 | db.session.commit() 64 | return task 65 | 66 | def get_tasks_in_progress(self): 67 | return Task.query.filter_by(user=self, complete=False).all() 68 | 69 | def get_task_in_progress(self, name): 70 | return Task.query.filter_by(name=name, user=self, complete=False).first() 71 | 72 | 73 | class Subscribe(db.Model): 74 | __tablename__ = 'subscribe' 75 | id = db.Column(db.Integer, primary_key=True) 76 | user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE', onupdate='CASCADE')) 77 | book_id = db.Column(db.String(128)) 78 | book_name = db.Column(db.String(128)) 79 | chapter = db.Column(db.String(128)) 80 | source_id = db.Column(db.String(128)) 81 | time = db.Column(db.DateTime, default=datetime.now()) 82 | chapter_name = db.Column(db.String(128)) 83 | 84 | user = db.relationship('User', backref=db.backref('subscribing', lazy='dynamic')) 85 | 86 | def __repr__(self): 87 | return '{%s} subscribing {%s} reading {%s}' % (self.user_id, self.book_id, self.chapter) 88 | 89 | 90 | # class Book(db.Model): 91 | # __tablename__ = 'book' 92 | # id = db.Column(db.Integer,primary_key=True) 93 | # _id = db.Column(db.String(50),unique=True) 94 | # name = db.Column(db.String(50)) 95 | # 96 | # subscribed = db.relationship('User', 97 | # secondary=subscribe, 98 | # primaryjoin=(subscribe.c.book_id==id), 99 | # backref=db.backref('subscribe',lazy='dynamic'), 100 | # lazy='dynamic') 101 | # 102 | # def __repr__(self): 103 | # return '' % self.name 104 | 105 | class Download(db.Model): 106 | __tablename__ = 'download' 107 | id = db.Column(db.Integer, primary_key=True) 108 | user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE', onupdate='CASCADE')) 109 | book_id = db.Column(db.String(128)) 110 | book_name = db.Column(db.String(128)) 111 | chapter = db.Column(db.Integer) 112 | source_id = db.Column(db.String(128)) 113 | time = db.Column(db.DateTime, default=datetime.now()) 114 | txt_name = db.Column(db.String(128)) 115 | lock = db.Column(db.Boolean) # 下载锁,1表示锁住,0表示开锁 116 | chapter_name = db.Column(db.String(128)) 117 | 118 | user = db.relationship('User', backref=db.backref('downloaded', lazy='dynamic')) 119 | 120 | def __repr__(self): 121 | return '{%s} download %s' % (self.user.name, self.book_name) 122 | 123 | 124 | class Task(db.Model): 125 | id = db.Column(db.String(36), primary_key=True) 126 | name = db.Column(db.String(128), index=True) 127 | description = db.Column(db.String(128)) 128 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 129 | complete = db.Column(db.Boolean, default=False) 130 | 131 | def get_rq_job(self): 132 | try: 133 | rq_job = rq.job.Job.fetch(self.id, connection=app.redis) 134 | except(redis.exceptions.RedisError, rq.exceptions.NoSuchJobError) as e: 135 | print(e) 136 | return None 137 | return rq_job 138 | 139 | def get_progress(self): 140 | job = self.get_rq_job() 141 | return job.meta.get('progress', 0) if job is not None else 100 142 | 143 | 144 | class Record(db.Model): 145 | id = db.Column(db.Integer, primary_key=True) 146 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 147 | book_id = db.Column(db.String(128)) 148 | book_name = db.Column(db.String(128)) 149 | chapter_index = db.Column(db.Integer) 150 | chapter_name = db.Column(db.String(128)) 151 | source_id = db.Column(db.String(128)) 152 | source_name = db.Column(db.String(128)) 153 | time = db.Column(db.DateTime, default=datetime.now()) 154 | is_subscribe = db.Column(db.Boolean) 155 | 156 | user = db.relationship('User', backref=db.backref('record', lazy='dynamic')) 157 | 158 | def __repr__(self): 159 | return '' & (self.user.name, self.book_name) 160 | 161 | 162 | @login.user_loader 163 | def load_user(id): 164 | user = User.query.get(int(id)) 165 | return user 166 | -------------------------------------------------------------------------------- /app/routes.py: -------------------------------------------------------------------------------- 1 | from app import app, db, text, redis 2 | from app.models import User, Subscribe, Download, Task, Record 3 | import json, os, re 4 | from flask import render_template, flash, redirect, url_for, request, jsonify, current_app 5 | from app.forms import LoginForm, RegistrationForm, SearchForm, JumpForm 6 | from flask_login import current_user, login_required, login_user, logout_user 7 | from werkzeug.urls import url_parse 8 | from datetime import datetime 9 | from time import time 10 | import requests 11 | from config import Config 12 | from hashlib import md5 13 | import asyncio, aiohttp 14 | import time 15 | from time import sleep 16 | 17 | 18 | def get_response(url): 19 | i = 0 20 | while i < 5: 21 | js = None 22 | try: 23 | data = requests.get(url, headers=Config.headers).text 24 | js = json.loads(data) 25 | break 26 | except: 27 | i += 1 28 | sleep(0.5) 29 | return js 30 | 31 | 32 | async def async_get_response(key, url, res): 33 | async with aiohttp.ClientSession() as session: 34 | async with session.get(url, headers=Config.headers) as resp: 35 | assert resp.status == 200 36 | res[key] = await resp.json() 37 | 38 | 39 | @app.before_request 40 | def before_request(): 41 | if current_user.is_authenticated: 42 | current_user.last_seen = datetime.utcnow() 43 | current_user.user_ip = request.headers.environ.get('REMOTE_ADDR') 44 | current_user.user_agent = request.headers.environ.get('HTTP_USER_AGENT') 45 | # 教程上说不需要加这一行,亲测需要 46 | db.session.add(current_user) 47 | db.session.commit() 48 | 49 | 50 | @app.route('/login', methods=['GET', 'POST']) 51 | def login(): 52 | if current_user.is_authenticated: 53 | return redirect(url_for('index')) 54 | form = LoginForm() 55 | if form.validate_on_submit(): 56 | u = User.query.filter_by(name=form.username.data).first() 57 | if u is None or not u.check_password(form.password.data): 58 | flash('登录失败') 59 | return redirect('login') 60 | login_user(u, remember=form.remember_me.data) 61 | # 网页回调,使用户登录后返回登录前页面 62 | next_page = request.args.get('next') 63 | if not next_page or url_parse(next_page).decode_netloc() != '': 64 | next_page = url_for('index') 65 | return redirect(next_page) 66 | flash('本站内容需要登录之后才能查看') 67 | return render_template('login.html', title='登录', form=form) 68 | 69 | 70 | @app.route('/logout') 71 | def logout(): 72 | logout_user() 73 | return redirect(url_for('index')) 74 | 75 | 76 | @app.route('/register', methods=['GET', 'POST']) 77 | def register(): 78 | form = RegistrationForm() 79 | if form.validate_on_submit(): 80 | u = User(name=form.username.data) 81 | u.set_password(form.password.data) 82 | db.session.add(u) 83 | db.session.commit() 84 | flash('注册成功') 85 | return redirect(url_for('login')) 86 | return render_template('register.html', form=form, title='注册') 87 | 88 | 89 | @app.route('/delete_user/', methods=['GET']) 90 | @login_required 91 | def delete_user(id): 92 | if not current_user.is_admin: 93 | return render_template('permission_denied.html', message=None, title='权限不足') 94 | u = User.query.get(id) 95 | db.session.delete(u) 96 | db.session.commit() 97 | flash('删除用户成功!') 98 | return redirect(url_for('user_list')) 99 | 100 | 101 | @app.route('/', methods=['GET', 'POST']) 102 | @app.route('/index', methods=['GET', 'POST']) 103 | # @login_required 104 | def index(): 105 | dic = {} 106 | subscribe_lis = list() 107 | res = dict() 108 | # 手动创建事件循环 109 | asyncio.set_event_loop(asyncio.new_event_loop()) 110 | loop = asyncio.get_event_loop() 111 | tasks = list() 112 | # 获取订阅信息 113 | if current_user.is_authenticated: 114 | dic['subscribe'] = [] 115 | for s in current_user.subscribing.order_by(Subscribe.time.desc()): 116 | subscribe_lis.append(s) 117 | if len(subscribe_lis) > 0: 118 | s_url = 'http://api.zhuishushenqi.com/book?view=updated&id=' 119 | for s in subscribe_lis: 120 | s_url += s.book_id + ',' 121 | s_url = s_url[:-1] 122 | tasks.append(async_get_response(key='subscribe', url=s_url, res=res)) 123 | 124 | # 获取分类 125 | # tasks.append(async_get_response(key='classify', url='https://novel.juhe.im/categories', res=res)) 126 | dic['classify'] = get_redis_string('classify') 127 | if not dic['classify']: 128 | tasks.append( 129 | async_get_response(key='classify', url='http://api.zhuishushenqi.com/cats/lv2/statistics', res=res)) 130 | else: 131 | dic['classify'] = json.loads(dic['classify']) 132 | 133 | # 获取榜单信息 134 | # tasks.append(async_get_response(key='rank', url='https://novel.juhe.im/rank-category', res=res)) 135 | dic['rank'] = get_redis_string('rank') 136 | if not dic['rank']: 137 | tasks.append(async_get_response(key='rank', url='http://api.zhuishushenqi.com/ranking/gender', res=res)) 138 | else: 139 | dic['rank'] = json.loads(dic['rank']) 140 | 141 | # 异步获取 142 | if len(tasks) > 0: 143 | loop.run_until_complete(asyncio.wait(tasks)) 144 | 145 | # 处理订阅信息 146 | js = res.get('subscribe') 147 | for i in range(0, len(subscribe_lis)): 148 | t = datetime.strptime(js[i]['updated'], UTC_FORMAT) 149 | dic['subscribe'].append({ 150 | 'title': subscribe_lis[i].book_name, 151 | '_id': subscribe_lis[i].book_id, 152 | 'last_chapter': js[i]['lastChapter'], 153 | 'updated': t 154 | }) 155 | # 预分组 156 | # data['male'] = [data['male'][i:i + 3] for i in range(0, len(data['male']), 3)] 157 | # data['female'] = [data['female'][i:i + 3] for i in range(0, len(data['female']), 3)] 158 | # data['press'] = [data['press'][i:i + 3] for i in range(0, len(data['press']), 3)] 159 | if not dic['classify']: 160 | dic['classify'] = res.get('classify') 161 | set_redis_string('classify', json.dumps(dic['classify'])) 162 | 163 | if not dic['rank']: 164 | dic['rank'] = res.get('rank') 165 | set_redis_string('rank', json.dumps(dic['rank'])) 166 | 167 | # 搜索框 168 | form = SearchForm() 169 | if form.validate_on_submit(): 170 | data = get_response('http://api.zhuishushenqi.com/book/fuzzy-search/?query=' + form.search.data) 171 | # data = get_response('http://novel.juhe.im/search?keyword=' + form.search.data) 172 | lis = [] 173 | for book in data.get('books'): 174 | lis.append(book) 175 | return render_template('search_result.html', data=lis, title='搜索结果', form=form) 176 | 177 | return render_template('index.html', data=dic, form=form, title='简阅', limit=Config.CHAPTER_PER_PAGE) 178 | 179 | 180 | @app.route('/subscribe/') 181 | @login_required 182 | def subscribe(): 183 | _id = request.args.get('id') 184 | # js = get_response('https://novel.juhe.im/book-info/' + _id) 185 | js = get_response('http://api.zhuishushenqi.com/book/' + _id) 186 | name = js.get('title') 187 | data = get_response('http://api.zhuishushenqi.com/toc?view=summary&book=' + _id) 188 | 189 | s = Subscribe(user=current_user, book_id=_id, book_name=name, source_id=data[1]['_id'], chapter=0) 190 | db.session.add(s) 191 | db.session.commit() 192 | flash('订阅成功') 193 | return redirect(url_for('book_detail', book_id=_id)) 194 | 195 | 196 | @app.route('/unsubscribe/') 197 | @login_required 198 | def unsubscribe(): 199 | _id = request.args.get('id') 200 | s = current_user.subscribing.filter(Subscribe.book_id == _id).first() 201 | db.session.delete(s) 202 | db.session.commit() 203 | flash('取消订阅成功') 204 | next_page = request.args.get('next') 205 | if not next_page or url_parse(next_page).decode_netloc() != '': 206 | next_page = url_for('index') 207 | return redirect(next_page) 208 | 209 | 210 | def get_source_id(book_id): 211 | dd = get_response('http://api.zhuishushenqi.com/toc?view=summary&book=' + book_id) 212 | source_id = None 213 | for i in range(len(dd))[::-1]: 214 | if dd[i]['source'] != 'zhuishuvip': 215 | source_id = dd[i]['_id'] 216 | if dd[i]['source'] == 'my176': 217 | break 218 | if not source_id: 219 | if len(dd) == 1 and dd[0]['source'] == 'zhuishuvip': 220 | source_id = dd[0]['_id'] 221 | return source_id 222 | 223 | 224 | @app.route('/chapter/', methods=['GET', 'POST']) 225 | @login_required 226 | def chapter(): 227 | page = request.args.get('page') 228 | book_id = request.args.get('book_id') 229 | source_id = request.args.get('source_id') 230 | if not source_id: 231 | source_id = get_source_id(book_id) 232 | data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id)) 233 | lis = [] 234 | l = [] 235 | chap = data.get('chapters') 236 | form = JumpForm() 237 | if form.validate_on_submit(): # 必须使用post方法才能正产传递参数 238 | page = form.page.data 239 | page_count = int(len(chap) / Config.CHAPTER_PER_PAGE) 240 | if len(chap) % Config.CHAPTER_PER_PAGE == 0: 241 | page_count -= 1 242 | if page is not None: 243 | page = int(page) 244 | if page > page_count: 245 | page = page_count 246 | lis = chap[page * Config.CHAPTER_PER_PAGE:(page + 1) * Config.CHAPTER_PER_PAGE] 247 | i = 0 248 | for c in lis: 249 | l.append({ 250 | 'index': page * Config.CHAPTER_PER_PAGE + i, 251 | 'title': c.get('title') 252 | }) 253 | i += 1 254 | 255 | if form.validate_on_submit(): 256 | return render_template('chapter.html', data=l, title='章节列表', page_count=page_count, page=form.page.data, 257 | source_id=source_id, 258 | book_id=book_id, form=form) 259 | 260 | return render_template('chapter.html', data=l, title='章节列表', page_count=page_count, page=page, source_id=source_id, 261 | book_id=book_id, form=form) 262 | 263 | 264 | @app.route('/read/', methods=['GET']) 265 | @login_required 266 | def read(): 267 | index = int(request.args.get('index')) 268 | source_id = request.args.get('source_id') 269 | book_id = request.args.get('book_id') 270 | # data = get_response('http://novel.juhe.im/book-chapters/' + source_id) 271 | r = Record(user=current_user, book_id=book_id, chapter_index=index, source_id=source_id, time=datetime.utcnow()) 272 | asyncio.set_event_loop(asyncio.new_event_loop()) 273 | loop = asyncio.get_event_loop() 274 | tasks = list() 275 | res = dict() 276 | tasks.append(async_get_response(key='detail', url='http://api.zhuishushenqi.com/book/' + book_id, res=res)) 277 | tasks.append( 278 | async_get_response(key='chapters', url='http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id), 279 | res=res)) 280 | loop.run_until_complete(asyncio.wait(tasks)) 281 | r.book_name = res['detail']['title'] 282 | r.source_name = res['chapters']['name'] 283 | # data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id)) 284 | page = int(index / Config.CHAPTER_PER_PAGE) 285 | chap = res['chapters'].get('chapters') 286 | title = chap[index]['title'] 287 | url = chap[index]['link'] 288 | r.chapter_name = chap[index]['title'] 289 | # 为各个源添加特殊处理 290 | if res['chapters']['source'] == 'biquge': 291 | url = reg_biquge(res['chapters']['link'], url) 292 | # 设定redis key 293 | key = md5((source_id + str(index)).encode("utf8")).hexdigest()[:10] 294 | 295 | # chapter_url = Config.CHAPTER_DETAIL.format(url.replace('/', '%2F').replace('?', '%3F')) 296 | # data = get_response(chapter_url) 297 | # if not data: 298 | # body = '检测到阅读接口发生故障,请刷新页面或稍后再试' 299 | # else: 300 | # if data['ok']: 301 | # body = data.get('chapter').get('cpContent') 302 | # else: 303 | # body = '此来源暂不可用,请换源' 304 | # if not body: 305 | # body = data.get('chapter').get('body') 306 | # lis = body.split('\n') 307 | # li = [] 308 | # for l in lis: 309 | # if l != '' and l != '\t': 310 | # li.append(l) 311 | li = get_content_list(key=key, url=url) 312 | if index < len(chap) - 1: 313 | next_key = md5((source_id + str(index + 1)).encode("utf8")).hexdigest()[:10] 314 | next_url = chap[index + 1]['link'] 315 | # 使用后台任务缓存下一章节 316 | try: 317 | current_app.task_queue.enqueue('app.tasks.cache', next_key, next_url) 318 | except: 319 | print('后台任务未开启!') 320 | font_size = '150%' 321 | if current_user.is_authenticated: 322 | font_size = current_user.font_size if current_user.font_size is not None else '150%' 323 | s = Subscribe.query.filter(Subscribe.book_id == book_id, Subscribe.user == current_user).first() 324 | if s: 325 | r.is_subscribe = True 326 | s.chapter = index 327 | s.chapter_name = title 328 | s.source_id = source_id 329 | s.time = datetime.utcnow() 330 | else: 331 | r.is_subscribe = False 332 | db.session.add(r) 333 | db.session.commit() 334 | 335 | return render_template('read.html', body=li, title=title, next=(index + 1) if len(chap) - index > 1 else None, 336 | pre=(index - 1) if index > 0 else None, index=index, 337 | book_id=book_id, page=page, source_id=source_id, font_size=font_size, 338 | background_color='#b0c4de') 339 | 340 | 341 | def reg_biquge(book_url, chapter_url): 342 | reg_normal = r'(http:\/\/www.biquge.la\/book\/[0-9]*\/[0-9]*.html)' 343 | reg_error = r'(http:\/\/www.biquge.la[0-9]*.html)' 344 | reg_chapter = r'([0-9]*.html)' 345 | reg = re.compile(reg_normal) 346 | lis = re.findall(reg, chapter_url) 347 | if lis: 348 | return chapter_url 349 | else: 350 | reg = re.compile(reg_error) 351 | lis = re.findall(reg, chapter_url) 352 | if lis: 353 | reg = re.compile(reg_chapter) 354 | lis = re.findall(reg, chapter_url) 355 | if lis: 356 | return book_url + lis[0] 357 | return chapter_url 358 | 359 | 360 | def get_content_text(url): 361 | chapter_url = Config.CHAPTER_DETAIL.format(url.replace('/', '%2F').replace('?', '%3F')) 362 | data = get_response(chapter_url) 363 | if not data: 364 | txt = '检测到阅读接口发生故障,请刷新页面或稍后再试' 365 | else: 366 | if data['ok']: 367 | txt = data.get('chapter').get('cpContent') 368 | else: 369 | txt = '此来源暂不可用,请换源' 370 | if not txt: 371 | txt = data.get('chapter').get('body') 372 | return txt 373 | 374 | 375 | def get_redis_string(key): 376 | if redis.exists(key): 377 | txt = redis.get(key).decode() 378 | else: 379 | return None 380 | return txt 381 | 382 | 383 | def set_redis_string(key, txt): 384 | redis.set(key, str(txt), ex=86400) 385 | 386 | 387 | def get_content_list(url, key=None): 388 | if key: 389 | txt = get_redis_string(key) 390 | if not key or not txt: 391 | txt = get_content_text(url) 392 | lis = txt.split('\n') 393 | li = [] 394 | for l in lis: 395 | if l != '' and l != '\t': 396 | li.append(l) 397 | return li 398 | 399 | 400 | # @app.route('/search/', methods=['GET', 'POST']) 401 | # def search(): 402 | # form = SearchForm() 403 | # if form.validate_on_submit(): 404 | # data = get_response('http://api.zhuishushenqi.com/book/fuzzy-search/?query=' + form.search.data) 405 | # lis = [] 406 | # for book in data.get('books'): 407 | # lis.append(book) 408 | # return render_template('search_result.html', data=lis, title='搜索结果') 409 | # return render_template('search_result.html', form=form, title='搜索') 410 | 411 | 412 | UTC_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 413 | LOCAL_FORMAT = '%Y-%m-%d %H:%M:%S' 414 | 415 | 416 | def utc2local(utc_st): 417 | now_stamp = time() 418 | local_time = datetime.fromtimestamp(now_stamp) 419 | utc_time = datetime.utcfromtimestamp(now_stamp) 420 | offset = local_time - utc_time 421 | local_st = utc_st + offset 422 | return local_st 423 | 424 | 425 | def local2utc(local_st): 426 | time_struct = time.mktime(local_st.timetuple()) 427 | utc_st = datetime.datetime.utcfromtimestamp(time_struct) 428 | return utc_st 429 | 430 | 431 | @app.route('/book_detail', methods=['GET']) 432 | @login_required 433 | def book_detail(): 434 | book_id = request.args.get('book_id') 435 | asyncio.set_event_loop(asyncio.new_event_loop()) 436 | c = 0 # 标识当前阅读章节序号 437 | is_subscribe = False 438 | loop = asyncio.get_event_loop() 439 | tasks = list() 440 | res = dict() 441 | tasks.append(async_get_response(key='detail', url='http://api.zhuishushenqi.com/book/' + book_id, res=res)) 442 | tasks.append( 443 | async_get_response(key='updated', url='http://api.zhuishushenqi.com/book?view=updated&id=' + book_id, res=res)) 444 | if current_user.is_authenticated: 445 | s = current_user.subscribing.filter(Subscribe.book_id == book_id).first() 446 | if s: 447 | source_id = s.source_id 448 | c = int(s.chapter) 449 | is_subscribe = True 450 | else: 451 | source_id = get_source_id(book_id) 452 | tasks.append(async_get_response( 453 | key='chapters', url='http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id), res=res)) 454 | loop.run_until_complete(asyncio.wait(tasks)) 455 | 456 | data = res.get('detail') 457 | t = datetime.strptime(res['updated'][0]['updated'], UTC_FORMAT) 458 | data['updated'] = t 459 | data['lastChapter'] = res['updated'][0]['lastChapter'] 460 | lis = data.get('longIntro').split('\n') 461 | data['longIntro'] = lis 462 | # if not res.get('chapters'): 463 | # return render_template('book_detail.html', data=data, title=data.get('title'), is_subscribe=False) 464 | # else: 465 | source_type = 'normal' 466 | lastIndex = None 467 | if res.get('chapters'): 468 | if res['chapters']['source'] == 'zhuishuvip': 469 | source_type = 'vip' 470 | chap = res['chapters']['chapters'] 471 | if chap[-1]['title'].split(' ')[-1] == data['lastChapter'].split(' ')[-1]: 472 | lastIndex = len(chap) - 1 473 | next = c + 1 if chap and len(chap) > c + 1 else None 474 | if c + 1 > len(chap): 475 | readingChapter = chap[-1]['title'] # 防止换源之后章节数量越界 476 | else: 477 | readingChapter = chap[c]['title'] 478 | 479 | else: 480 | source_type = None 481 | next = None 482 | c = None 483 | readingChapter = None 484 | 485 | return render_template( 486 | 'book_detail.html', data=data, lastIndex=lastIndex, reading=c, next=next, source_id=source_id, 487 | title=data.get('title'), readingChapter=readingChapter, is_subscribe=is_subscribe, source_type=source_type) 488 | 489 | 490 | @app.route('/source/', methods=['GET']) 491 | @login_required 492 | def source(book_id): 493 | page = request.args.get('page') 494 | # data = get_response('http://novel.juhe.im/book-sources?view=summary&book=' + book_id) 495 | data = get_response('http://api.zhuishushenqi.com/toc?view=summary&book=' + book_id) 496 | s = Subscribe.query.filter(Subscribe.user == current_user, Subscribe.book_id == book_id).first() 497 | source_id = '' 498 | if s: 499 | source_id = s.source_id 500 | for source in data: 501 | UTC_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 502 | t = datetime.strptime(source['updated'], UTC_FORMAT) 503 | source['updated'] = t 504 | if source_id == source['_id']: 505 | source['current'] = True 506 | else: 507 | source['current'] = False 508 | # t = s['updated'] 509 | # t = datetime.strptime(t, '%Y-%m-%dT%H:%M:%S.%fZ') 510 | # s['updated'] = utc2local(t).strftime('%Y-%m-%d %H:%M:%S') 511 | if not page: 512 | page = 0 513 | return render_template('source.html', data=data, title='换源', page=page, book_id=book_id) 514 | 515 | 516 | # 分类 517 | @app.route('/classify', methods=['GET']) 518 | @login_required 519 | def classify(): 520 | gender = request.args.get('gender') 521 | _type = request.args.get('type') 522 | major = request.args.get('major') 523 | start = request.args.get('start') 524 | # limit = request.args.get('limit') 525 | # page = request.args.get('page') 526 | # tag = request.args.get('tag') 527 | limit = str(Config.CHAPTER_PER_PAGE) 528 | # data = get_response( 529 | # 'https://novel.juhe.im/category-info?' + (('&major=' + major) if major else '') + ( 530 | # ('&gender=' + gender) if gender else '') + (('&type=' + _type) if _type else '') + ( 531 | # ('&start=' + start) if start else '') + (('&limit=' + limit) if limit else '')) 532 | data = get_response( 533 | 'http://api.zhuishushenqi.com/book/by-categories?' + (('&major=' + major) if major else '') + ( 534 | ('&gender=' + gender) if gender else '') + (('&type=' + _type) if _type else '') + ( 535 | ('&start=' + start) if start else '') + (('&limit=' + limit) if limit else '')) 536 | data = data['books'] 537 | next_page = True 538 | if len(data) < Config.CHAPTER_PER_PAGE: 539 | next_page = False 540 | return render_template('classify.html', data=data, title='探索', gender=gender, type=_type, major=major, 541 | start=int(start), 542 | limit=int(limit), next=next_page) 543 | 544 | 545 | # 书单列表 546 | @app.route('/book_list_rank', methods=['GET']) 547 | @login_required 548 | def book_list_rank(): 549 | gender = request.args.get('gender') 550 | duration = request.args.get('duration') 551 | start = request.args.get('start') 552 | sort = request.args.get('sort') 553 | limit = '20' 554 | # tag = request.args.get('tag') 555 | # data = get_response('https://novel.juhe.im/booklists?' + (('&gender=' + gender) if gender else '') + ( 556 | # ('&start=' + start) if start else '') + (('&duration=' + duration) if duration else '') + ( 557 | # ('&sort=' + sort) if sort else '') + (('&limit=' + limit) if limit else '')) 558 | data = get_response('http://api.zhuishushenqi.com/book-list?' + (('&gender=' + gender) if gender else '') + ( 559 | ('&start=' + start) if start else '') + (('&duration=' + duration) if duration else '') + ( 560 | ('&sort=' + sort) if sort else '') + (('&limit=' + limit) if limit else '')) 561 | next_page = False 562 | if data['total'] > 0: 563 | next_page = True 564 | return render_template('book_list_rank.html', data=data, title='书单排行', gender=gender, 565 | start=int(start), duration=duration, sort=sort, next_page=next_page, limit=20) 566 | 567 | 568 | # 书单详情 569 | @app.route('/bool_list_detail<_id>', methods=['GET']) 570 | @login_required 571 | def book_list_detail(_id): 572 | # data = get_response('https://novel.juhe.im/booklists/' + _id) 573 | data = get_response('http://api.zhuishushenqi.com/book-list/' + _id) 574 | UTC_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' 575 | updated = datetime.strptime(data['bookList']['updated'], UTC_FORMAT) 576 | created = datetime.strptime(data['bookList']['created'], UTC_FORMAT) 577 | data['bookList']['updated'] = updated 578 | data['bookList']['created'] = created 579 | return render_template('book_list_detail.html', data=data, title=data['bookList']['title']) 580 | 581 | 582 | # 排行榜 583 | @app.route('/rank/<_id>', methods=['GET']) 584 | @login_required 585 | def rank(_id): 586 | # data = get_response('http://novel.juhe.im/rank/' + _id) 587 | data = get_response('http://api.zhuishushenqi.com/ranking/' + _id) 588 | if data: 589 | return render_template('rank.html', title='排行', data=data) 590 | 591 | 592 | @app.route('/download', methods=['GET']) 593 | @login_required 594 | def download(): 595 | book_id = request.args.get('book_id') 596 | if not current_user.can_download: 597 | return render_template('permission_denied.html', title='权限不足', message='下载功能并非向所有人开放,请联系管理员索取权限') 598 | source_id = request.args.get('source_id') 599 | if not source_id: 600 | source_id = get_source_id(book_id) 601 | 602 | # data = get_response('http://novel.juhe.im/book-info/' + book_id) 603 | data = get_response('http://api.zhuishushenqi.com/book/' + book_id) 604 | book_name = data.get('title') 605 | 606 | d = Download.query.filter_by(book_id=book_id, source_id=source_id).first() 607 | 608 | # 检测资源锁 609 | if d: 610 | if d.lock: 611 | # 检测文件锁 612 | flash('文件正在生成,请稍后再试!') 613 | return redirect(url_for('book_detail', book_id=book_id)) 614 | else: 615 | # 检测服务器是否已经下载了文件的最新版本 616 | # data = get_response('http://novel.juhe.im/book-sources?view=summary&book=' + source_id) 617 | data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id)) 618 | chapter_list = data.get('chapters') 619 | download_list = chapter_list[d.chapter + 1:] 620 | 621 | if len(download_list) == 0: 622 | # 如果不存在新章节,返回文件链接 623 | book_title = d.book_name 624 | fileName = md5((book_id + source_id).encode("utf8")).hexdigest()[:10] + '.txt' 625 | return render_template('view_documents.html', title=book_title + '--下载', url=text.url(fileName), 626 | book_title=book_title) 627 | 628 | # from app.tasks import download 629 | # download(source_id,book_id) 630 | 631 | # 进入后台任务处理流程 632 | # if current_user.get_task_in_progress('download'): 633 | # flash('下载任务已经存在于您的任务列表当中!') 634 | # else: 635 | # 使用用户身份开启任务 636 | task = current_user.launch_task('download', book_name, source_id, book_id) 637 | db.session.commit() 638 | flash('下载任务已经提交,请稍后回来下载') 639 | return redirect(url_for('book_detail', book_id=book_id)) 640 | 641 | # # 获取章节信息 642 | # data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id)) 643 | # path = os.path.join(Config.UPLOADS_DEFAULT_DEST, 'downloads') 644 | # if not os.path.exists(path): 645 | # os.makedirs(path) 646 | # 647 | # # 定义文件名 648 | # fileName = md5((book_id + source_id).encode("utf8")).hexdigest()[:10] + '.txt' 649 | # # fileName = os.path.join(path, book_title + '.txt') 650 | # # if os.path.exists(fileName): 651 | # # os.remove(fileName) 652 | # 653 | # chapter_list = data.get('chapters') 654 | # if d: 655 | # # 截取需要下载的章节列表 656 | # new = False 657 | # download_list = chapter_list[d.chapter + 1:] 658 | # book_title = d.book_name 659 | # d.chapter = len(chapter_list) - 1 660 | # d.time = datetime.utcnow() 661 | # d.lock = True # 给下载加锁 662 | # d.chapter_name = chapter_list[len(chapter_list) - 1].get('title') 663 | # else: 664 | # new = True 665 | # # 获取书籍简介 666 | # data1 = get_response('http://api.zhuishushenqi.com/book/' + book_id) 667 | # book_title = data1.get('title') 668 | # author = data1.get('author') 669 | # longIntro = data1.get('longIntro') 670 | # download_list = chapter_list 671 | # d = Download(user=current_user, book_id=book_id, source_id=source_id, chapter=len(chapter_list) - 1, 672 | # book_name=book_title, time=datetime.utcnow(), txt_name=fileName, lock=True, 673 | # chapter_name=chapter_list[-1].get('title')) 674 | # 675 | # db.session.add(d) 676 | # db.session.commit() 677 | # 678 | # with open(os.path.join(path, fileName), 'a', encoding='gbk') as f: 679 | # if new: 680 | # f.writelines( 681 | # [' ', book_title, '\n', '\n', ' ', author, '\n', '\n', ' ', longIntro, '\n', '\n']) 682 | # for chapter in download_list: 683 | # title = chapter.get('title') 684 | # url = chapter.get('link') 685 | # # 为各个源添加特殊处理 686 | # if data['source'] == 'biquge': 687 | # url = reg_biquge(data['link'], url) 688 | # 689 | # li = get_text(url) 690 | # f.writelines(['\n', ' ', title, '\n', '\n']) 691 | # for sentence in li: 692 | # try: 693 | # f.writelines([' ', sentence, '\n', '\n']) 694 | # except: 695 | # pass 696 | # d.lock = False # 给下载解锁 697 | # db.session.add(d) 698 | # db.session.commit() 699 | # return render_template('view_documents.html', title=book_title + '--下载', url=text.url(fileName), 700 | # book_title=book_title) 701 | 702 | 703 | @app.route('/background', methods=['GET']) 704 | @login_required 705 | def background(): 706 | if not current_user.is_admin: 707 | return render_template('permission_denied.html', message=None, title='权限不足') 708 | return render_template('background.html', title='后台管理') 709 | 710 | 711 | @app.route('/user_list', methods=['GET']) 712 | @login_required 713 | def user_list(): 714 | if not current_user.is_admin: 715 | return render_template('permission_denied.html', message=None, title='权限不足') 716 | users = User.query.all() 717 | lis = list() 718 | for u in users: 719 | lis.append((u.id, u.name, u.is_admin, u.last_seen if u.last_seen else None)) 720 | 721 | return render_template('user_list.html', title='用户列表', lis=lis) 722 | 723 | 724 | @app.route('/user_detail/', methods=['GET']) 725 | @login_required 726 | def user_detail(id): 727 | if not current_user.is_admin: 728 | return render_template('permission_denied.html', message=None, title='权限不足') 729 | u = User.query.get(id) 730 | dic = { 731 | 'id': u.id, 732 | 'name': u.name, 733 | 'is_admin': u.is_admin, 734 | 'can_download': u.can_download, 735 | 'last_seen': u.last_seen if u.last_seen else None, 736 | 'user_agent': u.user_agent, 737 | 'user_ip': u.user_ip, 738 | } 739 | lis = list() 740 | for s in u.subscribing: 741 | lis.append({ 742 | 'book_id': s.book_id, 743 | 'book_name': s.book_name, 744 | 'source_id': s.source_id, 745 | 'chapter': s.chapter, 746 | 'chapter_name': s.chapter_name, 747 | 'time': s.time if s.time else None 748 | }) 749 | dic['subscribing'] = lis 750 | return render_template('user_detail.html', dic=dic, title='用户详情--%s' % u.name) 751 | 752 | 753 | @app.route('/change_download_permission/', methods=['GET']) 754 | @login_required 755 | def change_download_permission(id): 756 | if not current_user.is_admin: 757 | return render_template('permission_denied.html', message=None, title='权限不足') 758 | u = User.query.get(id) 759 | if u.can_download: 760 | u.can_download = False 761 | else: 762 | u.can_download = True 763 | db.session.add(u) 764 | db.session.commit() 765 | flash('修改下载权限成功!') 766 | return redirect(url_for('user_detail', id=id)) 767 | 768 | 769 | @app.route('/download_list', methods=['GET']) 770 | @login_required 771 | def download_list(): 772 | if not current_user.is_admin: 773 | return render_template('permission_denied.html', message=None, title='权限不足') 774 | ds = Download.query.all() 775 | lis = list() 776 | path = os.path.join(Config.UPLOADS_DEFAULT_DEST, 'downloads') 777 | for d in ds: 778 | # data = get_response('http://novel.juhe.im/book-sources?view=summary&book=' + d.source_id) 779 | data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(d.source_id)) 780 | source_name = data.get('name') 781 | if os.path.exists(os.path.join(path, d.txt_name)): 782 | txt_size = os.path.getsize(os.path.join(path, d.txt_name)) 783 | txt_size = txt_size / float(1024 * 1024) 784 | txt_size = round(txt_size, 2) 785 | else: 786 | txt_size = '文件缺失' 787 | lis.append({ 788 | 'id': d.id, 789 | 'user_id': d.user_id, 790 | 'user_name': d.user.name, 791 | 'book_name': d.book_name, 792 | 'book_id': d.book_id, 793 | 'chapter': d.chapter, 794 | 'source_id': d.source_id, 795 | 'source_name': source_name, 796 | 'time': d.time if d.time else None, 797 | 'txt_name': d.txt_name, 798 | 'chapter_name': d.chapter_name, 799 | 'txt_size': txt_size 800 | }) 801 | return render_template('download_list.html', lis=lis, title='下载列表') 802 | 803 | 804 | @app.route('/delete_download_file/', methods=['GET']) 805 | @login_required 806 | def delete_download_file(id): 807 | if not current_user.is_admin: 808 | return render_template('permission_denied.html', message=None, title='权限不足') 809 | d = Download.query.get(id) 810 | 811 | path = os.path.join(Config.UPLOADS_DEFAULT_DEST, 'downloads') 812 | if os.path.exists(os.path.join(path, d.txt_name)): 813 | os.remove(os.path.join(path, d.txt_name)) 814 | db.session.delete(d) 815 | db.session.commit() 816 | flash('删除下载项目成功!') 817 | return redirect(url_for('download_list')) 818 | 819 | 820 | @app.route('/download_file/', methods=['GET']) 821 | @login_required 822 | def download_file(): 823 | file_name = request.args.get('file_name') 824 | book_name = request.args.get('book_name') 825 | path = os.path.join(Config.UPLOADS_DEFAULT_DEST, 'downloads') 826 | if os.path.exists(os.path.join(path, file_name)): 827 | return render_template('view_documents.html', title='下载文件', url=text.url(file_name), book_title=book_name) 828 | 829 | 830 | @app.route('/get_task_progress', methods=['POST']) 831 | @login_required 832 | def get_task_progress(): 833 | ids = json.loads(request.get_data()) 834 | lis = [] 835 | for id in ids: 836 | task = Task.query.filter_by(id=id).first() 837 | lis.append({ 838 | 'id': task.id, 839 | 'progress': task.get_progress() 840 | }) 841 | return jsonify(lis) 842 | 843 | 844 | @app.route('/read_setting/', methods=['GET', 'POST']) 845 | @login_required 846 | def read_setting(): 847 | if request.method == 'GET': 848 | index = request.args.get('index') 849 | book_id = request.args.get('book_id=book_id') 850 | source_id = request.args.get('source_id') 851 | body = ['我们日复一日地生活于世,却对世界几乎一无所知。', 852 | '阳光的产生机制使生命得以实现;重力将我们束缚在地球上,不让我们以涡旋轨道被抛到太空;原子构成了我们的身躯,并使之保持稳定。', 853 | '对于这些,我们思考的很少。', 854 | '我们之中,很少有人会花时间惊讶自然界为何是这个样子:', 855 | '宇宙从何而来?', 856 | '或者它是否一直在这儿?', 857 | '时间会不会有朝一日倒流,并因此导致果先于因?', 858 | '人类的认知范围是否终有极限?', 859 | '物质的最小组成是什么?', 860 | '为什么我们记住的是过去,而不是未来?', 861 | '以及,为什么会有宇宙?'] 862 | next_url = url_for('read', index=index, book_id=book_id, source_id=source_id) 863 | return render_template('read_setting.html', title='阅读设置', body=body, next_url=next_url) 864 | if request.method == 'POST': 865 | data = json.loads(request.get_data()) 866 | font_size = data.get('font_size') 867 | night_mode = data.get('night_mode') 868 | current_user.font_size = font_size 869 | current_user.night_mode = night_mode 870 | db.session.commit() 871 | return 1 872 | 873 | 874 | @app.route('/author/', methods=['GET']) 875 | @login_required 876 | def author(author_name): 877 | # data = get_response('http://novel.juhe.im/author-books?author=' + author_name) 878 | data = get_response('http://api.zhuishushenqi.com/book/accurate-search?author=' + author_name) 879 | lis = list() 880 | for book in data['books']: 881 | lis.append({ 882 | '_id': book['_id'], 883 | 'title': book['title'], 884 | 'cover': book['cover'], 885 | 'retentionRatio': book['retentionRatio'], 886 | 'latelyFollower': book['latelyFollower'], 887 | 'author': author_name 888 | }) 889 | return render_template('author.html', title=author_name, lis=lis) 890 | -------------------------------------------------------------------------------- /app/static/read.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koala060528/LightReader/d5525e10e4b6ea211cb47e75b969bf6e5307b35f/app/static/read.ico -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | from app import app, db, redis 2 | from rq import get_current_job 3 | from app.models import Task, Download, User 4 | from app.routes import get_response, get_content_list, reg_biquge, get_content_text 5 | import os 6 | from config import Config 7 | from hashlib import md5 8 | from datetime import datetime 9 | 10 | app.app_context().push() 11 | 12 | 13 | def _set_task_progress(progress): 14 | job = get_current_job() 15 | if job: 16 | job.meta['progress'] = progress 17 | job.save_meta() 18 | task = Task.query.get(job.get_id()) 19 | # task.user.add_notification('task_progress', {'task_id': job.get_id(), 'progress': progress}) 20 | 21 | if progress >= 100: 22 | task.complete = True 23 | 24 | db.session.commit() 25 | 26 | 27 | def download(user_id, source_id, book_id): 28 | try: 29 | d = Download.query.filter_by(book_id=book_id, source_id=source_id).first() 30 | # 这里必须使用id查询user而不能直接使用current_user 31 | u = User.query.get(user_id) 32 | 33 | # 获取章节信息 34 | # data = get_response('http://novel.juhe.im/book-chapters/' + source_id) 35 | data = get_response('http://api.zhuishushenqi.com/toc/{0}?view=chapters'.format(source_id)) 36 | path = os.path.join(Config.UPLOADS_DEFAULT_DEST, 'downloads') 37 | if not os.path.exists(path): 38 | os.makedirs(path) 39 | 40 | # 定义文件名 41 | fileName = md5((book_id + source_id).encode("utf8")).hexdigest()[:10] + '.txt' 42 | # fileName = os.path.join(path, book_title + '.txt') 43 | # if os.path.exists(fileName): 44 | # os.remove(fileName) 45 | 46 | chapter_list = data.get('chapters') 47 | if d: 48 | # 截取需要下载的章节列表 49 | new = False 50 | download_list = chapter_list[d.chapter + 1:] 51 | book_title = d.book_name 52 | d.chapter = len(chapter_list) - 1 53 | d.time = datetime.utcnow() 54 | d.lock = True # 给下载加锁 55 | d.chapter_name = chapter_list[len(chapter_list) - 1].get('title') 56 | else: 57 | new = True 58 | # 获取书籍简介 59 | # data1 = get_response('http://novel.juhe.im/book-info/' + book_id) 60 | data1 = get_response('http://api.zhuishushenqi.com/book/' + book_id) 61 | book_title = data1.get('title') 62 | author = data1.get('author') 63 | longIntro = data1.get('longIntro') 64 | download_list = chapter_list 65 | 66 | d = Download(user=u, book_id=book_id, source_id=source_id, chapter=len(chapter_list) - 1, 67 | book_name=book_title, time=datetime.utcnow(), txt_name=fileName, lock=True, 68 | chapter_name=chapter_list[-1].get('title')) 69 | db.session.add(d) 70 | db.session.commit() 71 | 72 | with open(os.path.join(path, fileName), 'a', encoding='utf-8') as f: 73 | _set_task_progress(0) 74 | i = 0 75 | if new: 76 | f.writelines( 77 | [' ', book_title, '\n', '\n', ' ', author, '\n', '\n', ' ', longIntro, '\n', '\n']) 78 | for chapter in download_list: 79 | title = chapter.get('title') 80 | url = chapter.get('link') 81 | # 为各个源添加特殊处理 82 | if data['source'] == 'biquge': 83 | url = reg_biquge(data['link'], url) 84 | 85 | li = get_content_list(key=None, url=url) 86 | f.writelines(['\n', ' ', title, '\n', '\n']) 87 | for sentence in li: 88 | f.writelines([' ', sentence, '\n', '\n']) 89 | i += 1 90 | _set_task_progress(100 * i // len(download_list)) 91 | d.lock = False # 给下载解锁 92 | db.session.add(d) 93 | db.session.commit() 94 | except Exception as e: 95 | print(e) 96 | 97 | 98 | def cache(key, url): 99 | txt = get_content_text(url) 100 | # print(txt) 101 | redis.set(key, txt, ex=86400) 102 | -------------------------------------------------------------------------------- /app/templates/_book.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 13 | 14 | 15 | 17 | 18 |
4 | 5 | {{ book['title'] }} 6 | 7 |
11 | {{ book['author'] }} 12 |
热度:{{ book['latelyFollower'] }}   留存率:{{ book['retentionRatio'] }} 16 |
19 |
20 | -------------------------------------------------------------------------------- /app/templates/_book_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 |
4 | {{ bookList['title'] }} 5 | 6 |
10 | 书籍数量:{{ bookList['bookCount'] }}   收藏次数:{{ bookList['collectorCount'] }} 11 |
15 | 简介:{{ bookList['desc'] }} 16 |
19 |
-------------------------------------------------------------------------------- /app/templates/_book_list_book.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 |
4 | 5 | {{ book['book']['title'] }} 6 | 7 |
11 | {{ book['book']['author'] }} 12 |
热度:{{ book['book']['latelyFollower'] }}   留存率:{{ '%.2f' % book['book']['retentionRatio'] }}   字数:{{ book['book']['wordCount'] }} 16 |
评语:{{ book['comment'] }}
22 | 23 |
-------------------------------------------------------------------------------- /app/templates/_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ u[1] }} 5 | 6 |
7 | {% if u[2] %} 8 | ★ 9 | {% else %} 10 | ☆ 11 | {% endif %} 12 |    13 | 上次登录:{{ moment(u[3]).fromNow() }} 14 | 15 | 16 |
-------------------------------------------------------------------------------- /app/templates/author.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% if form %} 6 | {{ wtf.quick_form(form) }} 7 | {% endif %} 8 | {% if lis %} 9 | {% for book in lis %} 10 | {% include '_book.html' %} 11 | 12 | {% endfor %} 13 | {% endif %} 14 | {% endblock %} 15 | 16 | {% block navbar %} 17 | {{ super() }} 18 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/background.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

5 | 用户管理 6 |


7 |

8 | 下载管理 9 |


10 | 11 | {% endblock %} 12 | 13 | {% block navbar %} 14 | {{ super() }} 15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'bootstrap/base.html' %} 2 | 3 | {% block title %} 4 | {% if title %} 5 | {{ title }} 6 | {% else %} 7 | Novel Reader 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block navbar %} 12 | 25 | 56 | 57 | {% endblock %} 58 | 59 | {% block content %} 60 |
61 | {% if current_user.is_authenticated %} 62 | {% with tasks = current_user.get_tasks_in_progress() %} 63 | {% if tasks %} 64 | {% for task in tasks %} 65 | 70 | 71 | {% endfor %} 72 | 73 | {% endif %} 74 | {% endwith %} 75 | 76 | {% endif %} 77 | 78 | {% with messages = get_flashed_messages() %} 79 | {% if messages %} 80 | {% for message in messages %} 81 | 82 | {% endfor %} 83 | {% endif %} 84 | {% endwith %} 85 | 86 | {# application content needs to be provided in the app_content block #} 87 | {% block app_content %}{% endblock %} 88 |
89 | {% endblock %} 90 | 91 | {% block scripts %} 92 | {{ super() }} 93 | {{ moment.include_moment() }} 94 | {{ moment.lang('zh-CN') }} 95 | 96 | 144 | {% endblock %} 145 | -------------------------------------------------------------------------------- /app/templates/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 | {{ data.get('title') }} 5 |
6 |

作者:{{ data.get('author') }} 7 |

8 |

简介:

9 | {% for p in data.get('longIntro') %} 10 |

11 |         {{ p }} 12 |

13 | {% endfor %} 14 | {% if source_type !=None %} 15 | {% if source_type == 'vip' %} 16 |

17 | (提示:当前书源失效,请 换源 18 |

19 | {% endif %} 20 |

最近更新:{{ moment(data.get('updated')).fromNow() }}

21 | {% if lastIndex %} 22 |

最新章节: 23 | {{ data.get('lastChapter') }} 24 |

25 | {% else %} 26 |

最新章节:{{ data.get('lastChapter') }}

27 | {% endif %} 28 | 29 | 30 | {% if is_subscribe %} 31 |

阅读进度: {{ readingChapter }} 33 | {% if next %} 34 | 下一章 36 | {% endif %} 37 |

38 | {% endif %} 39 |

阅读选项: 40 | 章节列表   41 | 换源   42 | {% if is_subscribe %} 43 | 取消订阅 44 | {% else %} 45 | 订阅 46 | {% endif %} 47 |    48 | 下载 49 | 50 |

51 | {% else %} 52 |

53 | (提示:当前书源失效,请 换源 54 |

55 | {% endif %} 56 | 57 | 58 | {% endblock %} 59 | 60 | {% block navbar %} 61 | {{ super() }} 62 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/book_list_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 | {{ data['bookList']['title'] }} 5 |
6 |

作者:{{ data['bookList']['author']['nickname'] }} 7 |

8 |

简介:

9 | 10 |

11 |         {{ data['bookList']['desc'] }} 12 |

13 | 14 |

创建时间:{{ moment(data['bookList']['created']).fromNow() }}

15 |

最近更新:{{ moment(data['bookList']['updated']).fromNow() }}

16 | 17 |
18 | {% for book in data['bookList']['books'] %} 19 | {% include '_book_list_book.html' %} 20 | {% endfor %} 21 | {% endblock %} 22 | 23 | {% block navbar %} 24 | {{ super() }} 25 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/book_list_rank.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 | {% for bookList in data['bookLists'] %} 5 | {% include '_book_list.html' %} 6 | {% endfor %} 7 | 8 |
9 | 10 | 11 | 19 | 26 | 27 |
12 | {% if start > 0 %} 13 | 上一页 14 | {% else %} 15 | 上一页 16 | {% endif %} 17 |         18 | 20 | {% if next_page %} 21 | 下一页 22 | {% else %} 23 | 下一页 24 | {% endif %} 25 |
28 |
29 | {% endblock %} 30 | 31 | {% block navbar %} 32 | {{ super() }} 33 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/chapter.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

6 | {% for c in data %} 7 | {{ c.title }}
8 | {% endfor %} 9 |

10 |
11 | 12 | 13 | {% if page != 0 %} 14 | 首页 15 | {% else %} 16 | 首页 17 | {% endif %} 18 | 19 | 20 | {% if page != 0 %} 21 | 上一页 22 | {% else %} 23 | 上一页 24 | {% endif %} 25 | 26 | 27 | {% if page != page_count %} 28 | 下一页 29 | {% else %} 30 | 下一页 31 | {% endif %} 32 | 33 | 34 | {% if page != page_count %} 35 | 尾页 36 | {% else %} 37 | 尾页 38 | {% endif %} 39 | 40 | 41 | 42 | 43 |
44 | {# {{ wtf.quick_form(form) }}#} 45 | {# #} 46 | {# #} 47 | {{ form.hidden_tag() }} 48 | {{ form.page(size=10) }} 49 | {{ form.submit() }} 50 |
51 | 52 | 53 | {% endblock %} 54 | 55 | {% block navbar %} 56 | {{ super() }} 57 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/classify.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% if form %} 6 | {{ wtf.quick_form(form) }} 7 | {% endif %} 8 | {% if data %} 9 | {% for book in data %} 10 | {% include '_book.html' %} 11 | 12 | {% endfor %} 13 | {% endif %} 14 |
15 | 16 | 17 | 25 | 32 | 33 |
18 | {% if start > 0 %} 19 | 上一页 20 | {% else %} 21 | 上一页 22 | {% endif %} 23 |         24 | 26 | {% if next %} 27 | 下一页 28 | {% else %} 29 | 下一页 30 | {% endif %} 31 |
34 |
35 | {% endblock %} 36 | 37 | {% block navbar %} 38 | {{ super() }} 39 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/download_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

下载列表

6 |
7 | {% for d in lis %} 8 |

9 | {{ d['book_name'] }}--{{ d['source_name'] }}
10 | ·文件名:{{ d['txt_name'] }}  ·{{ d['txt_size'] }}MB  ·{{ d['time'] }}
11 | ·下载用户:{{ d['user_name'] }}  ·{% if d['chapter_name'] %} 12 | {{ d['chapter_name'] }} 13 | {% else %} 14 | {{ d['chapter'] }} 章 15 | {% endif %} 16 |
17 | ·下载 18 | ·删除 19 |

20 | 21 | 22 |
23 | {% endfor %} 24 | 25 | {% endblock %} 26 | 27 | {% block navbar %} 28 | {{ super() }} 29 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% if not current_user.is_anonymous %} 6 |

欢迎,{{ current_user.name }}

7 | {% else %} 8 |

欢迎,旅行者

9 | {% endif %} 10 |
11 |

订阅

12 | 13 | {% if not current_user.is_anonymous %} 14 | 15 | 16 | 17 | {#

订阅列表

#} 18 | {% if data.get('subscribe') %} 19 | {% for book in data.get('subscribe') %} 20 | 21 | 22 | 26 | 27 | 28 | 35 | 36 | 37 | 38 |
23 | {{ book['title'] }} 24 | 25 |
29 | {% if book['last_chapter'] %} 30 |

{{ book['last_chapter'] }}({{ moment(book['updated']).fromNow() }})

31 | {% else %} 32 |

获取更新失败,请重试

33 | {% endif %} 34 |
39 | 40 | {% endfor %} 41 | {% else %} 42 |

你的订阅列表是空的

43 | {% endif %} 44 | 45 | {% else %} 46 | 47 |

48 | 要查看你的订阅列表,请 登录,或 注册 49 |

50 | {% endif %} 51 |
52 | {% if form %} 53 |

探索

54 | {# {{ wtf.quick_form(form) }}#} 55 |
56 | {{ form.hidden_tag() }} 57 | {{ form.search(size=32) }} 58 | {{ form.submit() }} 59 |
60 |
61 | {% endif %} 62 | 63 | 64 | {% if data['classify'] %} 65 |

分类

66 | 67 |

男频

68 | 69 | {% for i in data['classify']['male'] %} 70 | 71 | 72 | {{ i['name'] }} 73 |   74 | 75 | 76 | {% endfor %} 77 | 78 |
79 |
80 | 81 |

女频

82 | 83 | {% for i in data['classify']['female'] %} 84 | 85 | 86 | {{ i['name'] }} 87 |   88 | 89 | 90 | {% endfor %} 91 | 92 |
93 |
94 | 95 |

出版

96 | 97 | {% for i in data['classify']['press'] %} 98 | 99 | 100 | {{ i['name'] }} 101 |   102 | 103 | 104 | {% endfor %} 105 | 106 |
107 | {% endif %} 108 | 109 | {% if data['rank'] %} 110 |
111 |

排行

112 | 113 |

男频

114 | 115 | {% for i in data['rank']['male'] %} 116 | 117 | {{ i['shortTitle'] }} 118 |   119 | 120 | {% endfor %} 121 | 122 |
123 |
124 | 125 |

女频

126 | 127 | {% for i in data['rank']['female'] %} 128 | 129 | {{ i['shortTitle'] }} 130 |   131 | 132 | {% endfor %} 133 | 134 |
135 | {% endif %} 136 | 137 | 138 |
139 |

书单

140 | 141 |

男频

142 | 143 | 最多收藏 144 |   145 | 146 | 147 | 本周最热 148 |   149 | 150 | 151 | 最新发布 152 | 153 |
154 |
155 | 156 |

女频

157 | 158 | 最多收藏 159 |   160 | 161 | 162 | 本周最热 163 |   164 | 165 | 166 | 最新发布 167 | 168 |
169 | 170 | 171 | {% endblock %} 172 | 173 | {% block navbar %} 174 | {{ super() }} 175 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | {% block app_content %} 4 |

登录

5 | {{ wtf.quick_form(form) }} 6 |
7 |

如果你没有账号,请注册

8 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/permission_denied.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

Permission Denied

5 |

{{ message }}

6 | 7 | 12 | {% endblock %} 13 | 14 | {% block navbar %} 15 | {{ super() }} 16 | 25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/rank.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% for book in data['ranking']['books'] %} 6 | {% include '_book.html' %} 7 | {% endfor %} 8 | {% endblock %} 9 | 10 | {% block navbar %} 11 | {{ super() }} 12 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/read.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

{{ title }}

5 |
6 |
7 | 8 | 9 | 18 | 19 | 24 | 25 | 33 | 34 |
10 | {% if pre is not none %} 11 | 上一章 13 | {% else %} 14 | 上一章 15 | {% endif %} 16 |         17 | 20 | 目录 22 |         23 | 26 | {% if next is not none %} 27 | 下一章 29 | {% else %} 30 | 下一章 31 | {% endif %} 32 |
35 |
36 |
37 | 38 | {% for p in body %} 39 |

40 |         {{ p }} 41 |

42 | {% endfor %} 43 | 44 |
45 |
46 | 47 | 48 | 57 | 58 | 63 | 64 | 72 | 73 |
49 | {% if pre is not none %} 50 | 上一章 52 | {% else %} 53 | 上一章 54 | {% endif %} 55 |         56 | 59 | 目录 61 |         62 | 65 | {% if next is not none %} 66 | 下一章 68 | {% else %} 69 | 下一章 70 | {% endif %} 71 |
74 |
75 |
76 | 77 | 78 | 82 | 87 | 92 | 95 | 96 |
79 | 详情 80 |        81 | 83 | 换源 85 |        86 | 88 | 设置 90 |        91 | 93 | 主页 94 |
97 |
98 | 99 | 100 | 101 | 102 | {% endblock %} 103 | 104 | {% block navbar %} 105 | {{ super() }} 106 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/read_setting.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |
5 | 6 | 7 |
8 | 9 |

阅读模式:

10 |
11 | 14 | 17 |
18 | 19 |

字体大小:

20 |
21 | 24 | 27 | 30 | 33 | 36 |
37 |
38 |

预览:

39 |
40 | 41 | {% for p in body %} 42 |

43 |         {{ p }} 44 |

45 | {% endfor %} 46 | 47 | 48 | 95 | {% endblock %} 96 | 97 | {% block navbar %} 98 | {{ super() }} 99 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | {% block app_content %} 4 |

注册

5 | {{ wtf.quick_form(form) }} 6 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/search_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% if form %} 6 | {{ wtf.quick_form(form) }} 7 | {% endif %} 8 | {% if data %} 9 | {% for book in data %} 10 | {% include '_book.html' %} 11 | 12 | {% endfor %} 13 | {% endif %} 14 | 15 | {% endblock %} 16 | 17 | {% block navbar %} 18 | {{ super() }} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/templates/source.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 | {% if data %} 5 | {% for s in data %} 6 | 7 | {{ s.get('name') }} 8 | {% if s.get('current')==True %} 9 | 10 | 11 | (当前选择) 12 | 13 | 14 | {% endif %} 15 | 16 | 17 |
18 | 19 | 最新章节:{{ s.get('lastChapter') }} -- 最近更新:{{ moment(s.get('updated')).fromNow() }} 20 | 21 |
22 | {% endfor %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | {% block navbar %} 27 | {{ super() }} 28 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/source_not_found.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

Source Not Found

5 |

{{ message }}

6 | 7 | 12 | {% endblock %} 13 | 14 | {% block navbar %} 15 | {{ super() }} 16 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

5 | 6 | 用户名:{{ dic['name'] }} 7 | 8 |

9 |
10 |

11 | 序号:{{ dic['id'] }} 12 |

13 |
14 |

15 | {% if dic['is_admin'] %} 16 | 管理员:是 17 | {% else %} 18 | 管理员:否 19 | {% endif %} 20 |

21 |
22 |

23 | {% if dic['can_download'] %} 24 | 能否下载:是 25 | {% else %} 26 | 能否下载:否 27 | {% endif %} 28 |    29 | 更改 30 |

31 |
32 |

33 | 最近活动:{{ moment(dic['last_seen'] ).fromNow() }} 34 |

35 |
36 |

37 | UA:{{ dic['user_agent'] }} 38 |

39 |
40 |

41 | IP:{{ dic['user_ip'] }} 42 |

43 |
44 |

45 | 删除用户 46 |

47 |
48 | 49 | 订阅列表: 50 | 51 |
52 |
53 | {% for s in dic['subscribing'] %} 54 | {{ s['book_name'] }}
55 | ·{% if s['chapter_name'] %} 56 | {{ s['chapter_name'] }} 57 | {% else %} 58 | {{ s['chapter'] }} 章 59 | {% endif %} 60 |    ·{{ moment(s['time'] ).fromNow() }}
61 |
62 | 63 | {% endfor %} 64 | 65 | 66 | {% endblock %} 67 | 68 | {% block navbar %} 69 | {{ super() }} 70 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 | {% if form %} 6 | {{ wtf.quick_form(form) }} 7 | {% endif %} 8 | {% if lis %} 9 | {% for u in lis %} 10 | {% include '_user.html' %} 11 | 12 | {% endfor %} 13 | {% endif %} 14 | 15 | {% endblock %} 16 | 17 | {% block navbar %} 18 | {{ super() }} 19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/view_documents.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block app_content %} 4 |

右键另存为!请使用utf-8格式解码txt文件!

5 |

{{ book_title }}

6 | 7 | 12 | {% endblock %} 13 | 14 | {% block navbar %} 15 | {{ super() }} 16 | {% endblock %} -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config(object): 5 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' 6 | # database config 7 | D_USER = os.environ.get('D_USER') or 'koala' 8 | D_PASSWORD = os.environ.get('D_PASSWORD') or '954193221' 9 | D_HOST = '127.0.0.1' 10 | D_PORT = 3306 11 | D_DATABASE = 'lightreader' 12 | # sql连接字符串 13 | SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://%s:%s@%s:%s/%s' % (D_USER, D_PASSWORD, D_HOST, D_PORT, D_DATABASE) 14 | SQLALCHEMY_TRACK_MODIFICATIONS = False 15 | 16 | # redis设置 17 | REDIS_URL = os.environ.get('REDIS_URL') or 'redis://' 18 | 19 | # 语言设置 20 | LANGUAGES = ['zh-CN'] 21 | 22 | # 每页显示的数量 23 | CHAPTER_PER_PAGE = 50 24 | 25 | # api url 26 | # 书籍详情页 27 | # BOOK_DETAIL = 'http://novel.juhe.im/book-info/{book_id}' 28 | BOOK_DETAIL = 'http://api.zhuishushenqi.com/book/{book_id}' 29 | # 章节列表 30 | # CHAPTER_LIST = 'http://novel.juhe.im/book-chapters/{book_id}' 31 | CHAPTER_LIST = 'http://api.zhuishushenqi.com/mix-atoc/{book_id}?view=chapters' 32 | # 章节详情 33 | # CHAPTER_DETAIL = 'http://novel.juhe.im/chapters/{0}' 34 | CHAPTER_DETAIL = 'http://chapter2.zhuishushenqi.com/chapter/{0}' 35 | 36 | # 文件目录 37 | UPLOADS_DEFAULT_DEST = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'files') 38 | 39 | # headers 40 | headers = { 41 | 'User-Agent': 'ZhuiShuShenQi/3.172.1 (Android 5.1.1; Meizu X86 / Meizu Mx5; China Mobile GSM)[preload=false;locale=zh_CN;clientidbase=]' 42 | } 43 | -------------------------------------------------------------------------------- /create_db.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | from config import Config 3 | from app import db 4 | 5 | conn = pymysql.connect(host=Config.D_HOST, port=Config.D_PORT, user=Config.D_USER, passwd=Config.D_PASSWORD) 6 | cursor = conn.cursor() 7 | cursor.execute("show databases like 'lightreader'") 8 | create_db = cursor.fetchall() 9 | if not create_db: 10 | cursor.execute('create database lightreader') 11 | cursor.close() 12 | conn.close() 13 | 14 | db.create_all() 15 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | app.run(debug=True, port=5001, host='127.0.0.1') 4 | -------------------------------------------------------------------------------- /debug_server.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | app.run(debug=True, port=5001, host='0.0.0.0') -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koala060528/LightReader/d5525e10e4b6ea211cb47e75b969bf6e5307b35f/favicon.ico -------------------------------------------------------------------------------- /lightreader.py: -------------------------------------------------------------------------------- 1 | from app import app, db 2 | from app.models import User, Subscribe, Download, Task 3 | 4 | 5 | @app.shell_context_processor 6 | def make_shell_context(): 7 | return {'db': db, 'User': User, 'Subscribe': Subscribe, 'Download': Download, 'Task': Task} 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.5.4 2 | alembic==1.0.10 3 | asn1crypto==0.24.0 4 | async-timeout==3.0.1 5 | asyncio==3.4.3 6 | attrs==19.1.0 7 | Babel==2.6.0 8 | certifi==2019.3.9 9 | cffi==1.12.3 10 | chardet==3.0.4 11 | Click==7.0 12 | cryptography==2.6.1 13 | dominate==2.3.5 14 | Flask==1.0.2 15 | Flask-Babel==0.12.2 16 | Flask-Bootstrap==3.3.7.1 17 | Flask-Login==0.4.1 18 | Flask-Migrate==2.4.0 19 | Flask-Moment==0.7.0 20 | Flask-SQLAlchemy==2.4.0 21 | Flask-Uploads==0.2.1 22 | Flask-WTF==0.14.2 23 | idna==2.8 24 | idna-ssl==1.1.0 25 | itsdangerous==1.1.0 26 | Jinja2==2.10.1 27 | Mako==1.0.9 28 | MarkupSafe==1.1.1 29 | multidict==4.5.2 30 | packaging==19.0 31 | pip-review==1.0 32 | pycparser==2.19 33 | PyMySQL==0.9.3 34 | pyparsing==2.4.0 35 | python-dateutil==2.8.0 36 | python-editor==1.0.4 37 | pytz==2019.1 38 | redis==3.2.1 39 | requests==2.21.0 40 | rq==1.0 41 | six==1.12.0 42 | SQLAlchemy==1.3.3 43 | urllib3==1.25.2 44 | visitor==0.1.3 45 | Werkzeug==0.14.1 46 | WTForms==2.2.1 47 | yarl==1.3.0 48 | --------------------------------------------------------------------------------