├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── env.py ├── ishuhui ├── __init__.py ├── controllers │ ├── __init__.py │ ├── admin.py │ ├── auth.py │ ├── comic.py │ └── error.py ├── csrf.py ├── data │ └── __init__.py ├── extensions │ ├── __init__.py │ ├── celeryext.py │ ├── flasksqlalchemy.py │ └── loginmanger.py ├── logger │ └── __init__.py ├── models │ ├── __init__.py │ ├── chapter.py │ ├── comic.py │ └── user.py ├── static │ ├── app.js │ └── style.css ├── tasks │ ├── __init__.py │ ├── celery_task.py │ └── task.py ├── templates │ ├── base │ │ ├── layout.html │ │ ├── msg.html │ │ └── navbar.html │ ├── chapters.html │ ├── comics.html │ ├── error.html │ ├── images.html │ ├── latest.html │ ├── login.html │ └── mange.html └── tmp │ └── .gitignore ├── logs └── .gitignore └── run.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=python -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | *.pyc 4 | *.log 5 | celerybeat* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 lufficc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Ishuhui! 3 |

4 | 5 | ## Ishuhui 6 | 7 | A flask project built for learning. Responsive waterfalls flow by [Masonry](https://masonry.desandro.com/) 8 | and real time search by [List.js](http://listjs.com). 9 | 10 | ### Features 11 | 12 | * Clear project structure. 13 | * Controllers, logger, scheduler, extensions, models, tasks etc. 14 | * Front end build with [Bootstrap4](https://github.com/twbs/bootstrap), [List.js](http://listjs.com), [Magnific Popup](http://dimsemenov.com/plugins/magnific-popup/), [Masonry](https://masonry.desandro.com/), [MDUI](https://www.mdui.org/), and [imagesLoaded](https://imagesloaded.desandro.com/). 15 | * Add login. 16 | * Message flash. 17 | * Using `celery` to load data asynchronously (Optional), with a progress dashboard. 18 | 19 | ### Dependencies 20 | 21 | - `flask_sqlalchemy` 22 | - `flask_login` 23 | - `celery` 24 | 25 | ### Usage 26 | 27 | 1. `git clone https://github.com/lufficc/flask_ishuhui.git` 28 | 1. `cd flask_ishuhui` 29 | 1. `python run.py` 30 | 1. Open localhost:5000 31 | 1. Login using `username` and `password`. 32 | 1. Set `ENABLE_CELERY` to `True` if you want to use celery. 33 | 1. Start celery by `celery -A ishuhui.tasks.celery_task.celery worker -B -E` in `flask_ishuhui` folde(same folder as `run.py`). 34 | 35 | NOTE: `username` and `password` are defined in [env.py](env.py) 36 | 37 | ### More screenshots 38 | 39 |

40 | Latest 41 | Latest 42 | One Piece 43 | One Piece 44 | Mange 45 | Mange Dashboard 46 |

47 | 48 | 49 | ### License 50 | 51 | Licensed under the [MIT License](LICENSE). 52 | -------------------------------------------------------------------------------- /env.py: -------------------------------------------------------------------------------- 1 | import os 2 | APP_PATH = os.path.dirname(os.path.abspath(__file__)) + '/ishuhui' 3 | HOST = '0.0.0.0' 4 | PORT = 5000 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI='sqlite:///' + APP_PATH + '/tmp/ishuhui.db' 7 | SECRET_KEY='7c401a1e5fd54c6cd8cd0d5016c2911157a6127815ab7686' 8 | USERNAME='lufficc' 9 | PASSWORD='123456' 10 | ENABLE_CELERY=False 11 | CELERY_BROKER_URL='redis://localhost:6379/0' 12 | CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' 13 | -------------------------------------------------------------------------------- /ishuhui/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from . import csrf 4 | 5 | 6 | def create_app(config, should_register_blueprints=True): 7 | app = Flask(__name__) 8 | app.config.from_object(config) 9 | app.config.from_envvar('FLASKR_SETTINGS', silent=True) 10 | from ishuhui.extensions.loginmanger import login_manager 11 | from ishuhui.extensions.flasksqlalchemy import db 12 | login_manager.setup_app(app) 13 | db.init_app(app) 14 | 15 | csrf.init(app) 16 | 17 | from ishuhui.logger import init_logger 18 | init_logger(app) 19 | 20 | if should_register_blueprints: 21 | register_blueprints(app) 22 | 23 | with app.app_context(): 24 | db.create_all() 25 | return app 26 | 27 | 28 | def register_blueprints(app): 29 | from ishuhui.controllers.comic import bp_comic 30 | app.register_blueprint(bp_comic) 31 | 32 | from ishuhui.controllers.admin import bp_admin 33 | app.register_blueprint(bp_admin) 34 | 35 | from ishuhui.controllers.auth import bp_auth 36 | app.register_blueprint(bp_auth) 37 | 38 | from ishuhui.controllers.error import bp_error 39 | app.register_blueprint(bp_error) 40 | -------------------------------------------------------------------------------- /ishuhui/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lufficc/flask_ishuhui/a3444b3679c45d5ba94c5c9a66551207eff1a646/ishuhui/controllers/__init__.py -------------------------------------------------------------------------------- /ishuhui/controllers/admin.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, current_app, session 2 | from flask import abort, jsonify 3 | from flask_login import current_user 4 | 5 | import ishuhui.tasks.task as task 6 | from ..models.chapter import Chapter 7 | from ..models.comic import Comic 8 | from ..tasks.celery_task import refresh_chapters_task 9 | 10 | bp_admin = Blueprint('admin', __name__, url_prefix='/admin') 11 | 12 | 13 | @bp_admin.before_request 14 | def login(): 15 | if not current_user.is_authenticated: 16 | abort(403) 17 | 18 | 19 | @bp_admin.route('/mange', methods=['GET']) 20 | def mange(): 21 | return render_template('mange.html', chapter_count=Chapter.query.count(), 22 | comic_count=Comic.query.count(), 23 | comics=Comic.query.all(), 24 | task_id=session.get('task_id'), 25 | enable_celery=current_app.config['ENABLE_CELERY'], 26 | running=session.get('task_id') is not None) 27 | 28 | 29 | @bp_admin.route('/refresh_comics') 30 | def refresh_comics(): 31 | return jsonify(task.refresh_comics()) 32 | 33 | 34 | @bp_admin.route('/refresh_chapters') 35 | def refresh_chapters(): 36 | if current_app.config['ENABLE_CELERY']: 37 | if session.get('task_id') is None: 38 | t = refresh_chapters_task.apply_async() 39 | session['task_id'] = t.id 40 | return session['task_id'] 41 | else: 42 | result = refresh_chapters_task.AsyncResult(session['task_id']) 43 | if result.state == 'SUCCESS' or result.state == 'FAILURE': 44 | t = refresh_chapters_task.apply_async() 45 | session['task_id'] = t.id 46 | return session['task_id'] 47 | return 'Already running', 400 48 | return jsonify(task.refresh_chapters()) 49 | 50 | 51 | @bp_admin.route('/tasks/status/') 52 | def task_status(task_id): 53 | result = refresh_chapters_task.AsyncResult(task_id) 54 | if result.state == 'PENDING': 55 | response = { 56 | 'state': result.state, 57 | 'progress': 0, 58 | } 59 | elif result.state != 'FAILURE': 60 | response = { 61 | 'state': result.state, 62 | 'progress': result.info.get('progress', 0), 63 | } 64 | if result.state == 'SUCCESS': 65 | session.pop('task_id') 66 | if 'result' in result.info: 67 | response['result'] = result.info['result'] 68 | else: 69 | # something went wrong in the background job 70 | session.pop('task_id') 71 | response = { 72 | 'state': result.state, 73 | 'progress': 0, 74 | 'status': str(result.info), # this is the exception raised 75 | } 76 | return jsonify(response) 77 | 78 | 79 | @bp_admin.route('/refresh_comic_images') 80 | def refresh_comic_images(): 81 | return jsonify(task.refresh_comic_images()) 82 | -------------------------------------------------------------------------------- /ishuhui/controllers/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, abort, current_app, flash, redirect, session, url_for 2 | from flask import request 3 | from flask_login import login_user, logout_user, current_user 4 | 5 | from ..models.user import User 6 | 7 | bp_auth = Blueprint('auth', __name__) 8 | 9 | 10 | @bp_auth.before_request 11 | def csrf_protect(): 12 | if request.method == "POST": 13 | token = session.pop('_csrf_token', None) 14 | request_token = request.form.get('_csrf_token') 15 | if not token or token != request_token: 16 | abort(403) 17 | 18 | 19 | @bp_auth.route('/login', methods=['GET', 'POST']) 20 | def login(): 21 | if request.method == 'GET' and not current_user.is_authenticated: 22 | return render_template('login.html') 23 | elif request.method == 'POST': 24 | username = request.form.get('username') 25 | password = request.form.get('password') 26 | if current_app.config['USERNAME'] != username or current_app.config['PASSWORD'] != password: 27 | flash('Login failed!', 'danger') 28 | return redirect(request.url) 29 | else: 30 | login_user(User()) 31 | flash('Login succeed!', 'success') 32 | return redirect(url_for('admin.mange')) 33 | return redirect('/') 34 | 35 | 36 | @bp_auth.route('/logout', methods=['GET']) 37 | def logout(): 38 | if not current_user.is_authenticated: 39 | abort(403) 40 | logout_user() 41 | return redirect('/') 42 | -------------------------------------------------------------------------------- /ishuhui/controllers/comic.py: -------------------------------------------------------------------------------- 1 | import re, requests 2 | from flask import render_template, Blueprint, json, abort, redirect, request, jsonify, url_for, flash 3 | 4 | import ishuhui.data as data 5 | from ishuhui.extensions.flasksqlalchemy import db 6 | 7 | bp_comic = Blueprint('comic', __name__) 8 | 9 | 10 | @bp_comic.route('/') 11 | def latest_chapters(): 12 | chapters = data.get_latest_chapters(12) 13 | return render_template('latest.html', comic=None, chapters=chapters) 14 | 15 | 16 | @bp_comic.route('/comics') 17 | def comics(): 18 | classify_id = request.args.get('classify_id') 19 | comics = data.get_comics(classify_id) 20 | return render_template('comics.html', comics=comics) 21 | 22 | 23 | @bp_comic.route('/comics//chapters') 24 | def chapters(comic_id): 25 | comic = data.get_comic(comic_id) 26 | chapters = data.get_chapters(comic_id) 27 | return render_template('chapters.html', comic=comic, chapters=chapters) 28 | 29 | 30 | image_pattern = re.compile(r']*src="([^"]+)') 31 | 32 | 33 | def get_images_from_url(url): 34 | html = requests.get(url).text 35 | images = image_pattern.findall(html) 36 | return images 37 | 38 | 39 | @bp_comic.route('/refresh_chapters/', methods=['GET']) 40 | def refresh_chapter(chapter_id): 41 | chapter = data.get_chapter(chapter_id) 42 | url = 'http://www.ishuhui.net/ComicBooks/ReadComicBooksToIsoV1/' + str(chapter_id) + '.html' 43 | images = get_images_from_url(url) 44 | chapter.images = json.dumps(images) 45 | db.session.commit() 46 | flash('Refresh succeed!', 'success') 47 | return redirect(url_for('comic.chapter', comic_id=chapter.comic().id, chapter_id=chapter.id)) 48 | 49 | 50 | @bp_comic.route('/comics//chapters/') 51 | def chapter(comic_id, chapter_id): 52 | comic = data.get_comic(comic_id) 53 | chapter = data.get_chapter(chapter_id) 54 | next_chapter = data.get_next_chapter(comic_id, chapter.chapter_number) 55 | prev_chapter = data.get_prev_chapter(comic_id, chapter.chapter_number) 56 | url = 'http://www.ishuhui.net/ComicBooks/ReadComicBooksToIsoV1/' + str( 57 | chapter_id) + '.html' 58 | if chapter.comic_id != comic_id: 59 | abort(404) 60 | if chapter.images: 61 | images = json.loads(chapter.images) 62 | else: 63 | images = get_images_from_url(url) 64 | chapter.images = json.dumps(images) 65 | db.session.commit() 66 | return render_template( 67 | 'images.html', 68 | comic=comic, 69 | chapter=chapter, 70 | next_chapter=next_chapter, 71 | prev_chapter=prev_chapter, 72 | images=images, 73 | url=url) 74 | -------------------------------------------------------------------------------- /ishuhui/controllers/error.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, Blueprint 2 | 3 | bp_error = Blueprint('error', __name__, template_folder="../templates/error") 4 | 5 | 6 | @bp_error.app_errorhandler(403) 7 | def page_not_found(error): 8 | return render_template('error.html', error=error), 403 9 | 10 | 11 | @bp_error.app_errorhandler(404) 12 | def page_not_found(error): 13 | return render_template('error.html', error=error), 404 14 | 15 | 16 | @bp_error.app_errorhandler(500) 17 | def server_error(error): 18 | return render_template('error.html', error=error), 500 19 | -------------------------------------------------------------------------------- /ishuhui/csrf.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import os 3 | 4 | from flask import session 5 | 6 | 7 | def init(app): 8 | def generate_csrf_token(): 9 | if '_csrf_token' not in session: 10 | session['_csrf_token'] = binascii.b2a_hex(os.urandom(15)).decode("utf-8") 11 | return session['_csrf_token'] 12 | 13 | app.jinja_env.globals['csrf_token'] = generate_csrf_token 14 | -------------------------------------------------------------------------------- /ishuhui/data/__init__.py: -------------------------------------------------------------------------------- 1 | from ishuhui.models.chapter import Chapter 2 | from ishuhui.models.comic import Comic 3 | from sqlalchemy import and_ 4 | 5 | 6 | def get_comics(classify_id=None): 7 | if classify_id is None: 8 | return Comic.query.all() 9 | else: 10 | return Comic.query.filter_by(classify_id=classify_id).all() 11 | 12 | 13 | def get_comic(comic_id): 14 | return Comic.query.get(comic_id) 15 | 16 | 17 | def count_chapters(comic_id=None): 18 | if comic_id is None: 19 | return Chapter.query.count() 20 | return Chapter.query.filter_by(comic_id=comic_id).count() 21 | 22 | 23 | def get_chapters(comic_id=None): 24 | if comic_id is None: 25 | return Chapter.query.order_by(Chapter.chapter_number.desc()).all() 26 | return Chapter.query.filter_by(comic_id=comic_id).order_by(Chapter.chapter_number.desc()).all() 27 | 28 | 29 | def get_next_chapter(comic_id, chapter_number): 30 | chapter = Chapter.query.filter( 31 | and_(Chapter.comic_id == comic_id, Chapter.chapter_number > chapter_number)).order_by( 32 | Chapter.chapter_number.asc()).first() 33 | return chapter 34 | 35 | 36 | def get_prev_chapter(comic_id, chapter_number): 37 | chapter = Chapter.query.filter( 38 | and_(Chapter.comic_id == comic_id, Chapter.chapter_number < chapter_number)).order_by( 39 | Chapter.chapter_number.desc()).first() 40 | return chapter 41 | 42 | 43 | def get_chapter(chapter_id): 44 | return Chapter.query.get(chapter_id) 45 | 46 | 47 | def get_latest_chapters(cnt=10): 48 | return Chapter.query.order_by(Chapter.refresh_time.desc()).limit(cnt).all() 49 | -------------------------------------------------------------------------------- /ishuhui/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lufficc/flask_ishuhui/a3444b3679c45d5ba94c5c9a66551207eff1a646/ishuhui/extensions/__init__.py -------------------------------------------------------------------------------- /ishuhui/extensions/celeryext.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | 3 | 4 | def create_celery(app): 5 | celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], broker=app.config['CELERY_BROKER_URL']) 6 | celery.conf.update(app.config) 7 | return celery 8 | -------------------------------------------------------------------------------- /ishuhui/extensions/flasksqlalchemy.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() -------------------------------------------------------------------------------- /ishuhui/extensions/loginmanger.py: -------------------------------------------------------------------------------- 1 | from flask_login import LoginManager 2 | 3 | from ..models.user import User 4 | 5 | login_manager = LoginManager() 6 | 7 | 8 | @login_manager.user_loader 9 | def load_user(user_id): 10 | return User() 11 | -------------------------------------------------------------------------------- /ishuhui/logger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import RotatingFileHandler 3 | 4 | 5 | def init_logger(app): 6 | handler = RotatingFileHandler('logs/ishuhui.log', maxBytes=1024 * 1024 * 2, backupCount=2) 7 | logging_format = logging.Formatter( 8 | '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]') 9 | handler.setFormatter(logging_format) 10 | app.logger.addHandler(handler) 11 | app.logger.setLevel(logging.INFO) 12 | -------------------------------------------------------------------------------- /ishuhui/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lufficc/flask_ishuhui/a3444b3679c45d5ba94c5c9a66551207eff1a646/ishuhui/models/__init__.py -------------------------------------------------------------------------------- /ishuhui/models/chapter.py: -------------------------------------------------------------------------------- 1 | from ishuhui.extensions.flasksqlalchemy import db 2 | import ishuhui.data 3 | 4 | 5 | class Chapter(db.Model): 6 | __tablename__ = 'chapters' 7 | id = db.Column(db.Integer, primary_key=True, unique=True) 8 | title = db.Column(db.String(256), nullable=False) 9 | comic_id = db.Column(db.Integer, nullable=False) 10 | chapter_number = db.Column(db.Integer, nullable=False) 11 | front_cover = db.Column(db.String(256), nullable=True) 12 | refresh_time = db.Column(db.DateTime, nullable=True) 13 | images = db.Column(db.Text, nullable=True) 14 | 15 | def comic(self): 16 | return ishuhui.data.get_comic(self.comic_id) 17 | -------------------------------------------------------------------------------- /ishuhui/models/comic.py: -------------------------------------------------------------------------------- 1 | import ishuhui.data 2 | from ishuhui.extensions.flasksqlalchemy import db 3 | 4 | 5 | class Comic(db.Model): 6 | __tablename__ = 'comics' 7 | id = db.Column(db.Integer, primary_key=True, unique=True) 8 | title = db.Column(db.String(256), nullable=False) 9 | description = db.Column(db.String(1024), nullable=False) 10 | refresh_time = db.Column(db.DateTime, nullable=True) 11 | author = db.Column(db.String(256), nullable=True) 12 | classify_id = db.Column(db.Integer, nullable=False) 13 | front_cover = db.Column(db.String(256), nullable=True) 14 | 15 | @property 16 | def chapters_count(self): 17 | return ishuhui.data.count_chapters(self.id) 18 | -------------------------------------------------------------------------------- /ishuhui/models/user.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from flask_login import UserMixin 3 | 4 | 5 | class User(UserMixin): 6 | def get_id(self): 7 | return '1' 8 | 9 | @property 10 | def name(self): 11 | return current_app.config['USERNAME'] 12 | -------------------------------------------------------------------------------- /ishuhui/static/app.js: -------------------------------------------------------------------------------- 1 | function messge(msg, type) { 2 | var snackbar = $("#snackbar"); 3 | snackbar.text(msg); 4 | if (type === 'error') { 5 | snackbar.css("background-color", "#e15e35"); 6 | } else { 7 | snackbar.css("background-color", "#333"); 8 | } 9 | snackbar.attr('class', 'show'); 10 | setTimeout(function () { 11 | snackbar.attr('class', ''); 12 | }, 3000); 13 | } -------------------------------------------------------------------------------- /ishuhui/static/style.css: -------------------------------------------------------------------------------- 1 | 2 | .card { 3 | border: none; 4 | } 5 | 6 | .bg-transparent { 7 | background-color: transparent !important; 8 | } 9 | 10 | .bg-half-transparent { 11 | background-color: rgba(0, 0, 0, 0.2) !important; 12 | } 13 | 14 | .chapters-content { 15 | background-color: transparent; 16 | } 17 | 18 | .chapters-img { 19 | position: absolute; 20 | left: 0; 21 | right: 0; 22 | width: 100%; 23 | z-index: -1; 24 | object-fit: cover; 25 | filter: blur(15px); 26 | -webkit-filter: blur(15px); 27 | } 28 | 29 | /* The snackbar - position it at the bottom and in the middle of the screen */ 30 | #snackbar { 31 | visibility: hidden; /* Hidden by default. Visible on click */ 32 | min-width: 250px; /* Set a default minimum width */ 33 | margin-left: -125px; /* Divide value of min-width by 2 */ 34 | background-color: #333; /* Black background color */ 35 | color: #fff; /* White text color */ 36 | text-align: center; /* Centered text */ 37 | border-radius: 3px; /* Rounded borders */ 38 | padding: 16px; /* Padding */ 39 | position: fixed; /* Sit on top of the screen */ 40 | z-index: 1; /* Add a z-index if needed */ 41 | left: 50%; /* Center the snackbar */ 42 | bottom: 30px; /* 30px from the bottom */ 43 | } 44 | 45 | /* Show the snackbar when clicking on a button (class added with JavaScript) */ 46 | #snackbar.show { 47 | visibility: visible; /* Show the snackbar */ 48 | 49 | /* Add animation: Take 0.5 seconds to fade in and out the snackbar. 50 | However, delay the fade out process for 2.5 seconds */ 51 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; 52 | animation: fadein 0.5s, fadeout 0.5s 2.5s; 53 | } 54 | 55 | /* Animations to fade the snackbar in and out */ 56 | @-webkit-keyframes fadein { 57 | from { 58 | bottom: 0; 59 | opacity: 0; 60 | } 61 | to { 62 | bottom: 30px; 63 | opacity: 1; 64 | } 65 | } 66 | 67 | @keyframes fadein { 68 | from { 69 | bottom: 0; 70 | opacity: 0; 71 | } 72 | to { 73 | bottom: 30px; 74 | opacity: 1; 75 | } 76 | } 77 | 78 | @-webkit-keyframes fadeout { 79 | from { 80 | bottom: 30px; 81 | opacity: 1; 82 | } 83 | to { 84 | bottom: 0; 85 | opacity: 0; 86 | } 87 | } 88 | 89 | @keyframes fadeout { 90 | from { 91 | bottom: 30px; 92 | opacity: 1; 93 | } 94 | to { 95 | bottom: 0; 96 | opacity: 0; 97 | } 98 | } -------------------------------------------------------------------------------- /ishuhui/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lufficc/flask_ishuhui/a3444b3679c45d5ba94c5c9a66551207eff1a646/ishuhui/tasks/__init__.py -------------------------------------------------------------------------------- /ishuhui/tasks/celery_task.py: -------------------------------------------------------------------------------- 1 | from celery.schedules import crontab 2 | from celery.task import periodic_task 3 | 4 | from .. import create_app 5 | from ..extensions.celeryext import create_celery 6 | from ..tasks import task 7 | 8 | app = create_app('env', False) 9 | celery = create_celery(app) 10 | 11 | 12 | @periodic_task(run_every=(crontab(minute='*/50')), name="scheduled_refresh_chapters_task", ignore_result=True) 13 | def scheduled_refresh_chapters_task(): 14 | with app.app_context(): 15 | task.refresh_chapters() 16 | 17 | 18 | @celery.task(bind=True) 19 | def refresh_chapters_task(self): 20 | def listener(current, total, result): 21 | self.update_state(state='PROGRESS', meta={'progress': current / total}) 22 | 23 | with app.app_context(): 24 | return {'progress': 1, 'result': task.refresh_chapters(listener)} 25 | -------------------------------------------------------------------------------- /ishuhui/tasks/task.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | import requests 5 | from flask import current_app 6 | from sqlalchemy import and_ 7 | 8 | import ishuhui.data as data 9 | from ishuhui.extensions.flasksqlalchemy import db 10 | from ishuhui.models.chapter import Chapter 11 | from ishuhui.models.comic import Comic 12 | 13 | 14 | def load_comics(page): 15 | response = requests.get( 16 | "http://www.ishuhui.net/ComicBooks/GetAllBook", 17 | params={"PageIndex": page}) 18 | return response.json()['Return']['List'] 19 | 20 | 21 | def parse_date(time_str): 22 | """ 23 | :param time_str: like "/Date(1453196817000)/" 24 | :return: datetime 25 | """ 26 | timestamp = int(time_str[6:-2]) 27 | return datetime.datetime.fromtimestamp(timestamp / 1e3) 28 | 29 | 30 | def refresh_comics(): 31 | page = 0 32 | comics = load_comics(page) 33 | current_app.logger.info('get {} comics of page {}'.format(len(comics), page)) 34 | result = [] 35 | while len(comics) > 0: 36 | for comic in comics: 37 | try: 38 | if Comic.query.get(comic['Id']): 39 | current_app.logger.info('comic {} already existed'.format(comic['Id'])) 40 | continue 41 | new_comic = Comic() 42 | new_comic.id = comic['Id'] 43 | new_comic.title = comic['Title'] 44 | new_comic.description = comic['Explain'] 45 | new_comic.refresh_time = parse_date(comic['RefreshTime']) 46 | new_comic.author = comic['Author'] 47 | new_comic.classify_id = comic['ClassifyId'] 48 | new_comic.front_cover = comic['FrontCover'] 49 | db.session.add(new_comic) 50 | db.session.commit() 51 | result.append(comic['Id']) 52 | except Exception as e: 53 | current_app.logger.error('exception occur when save comic {} :{}'.format(comic['Id'], e)) 54 | page += 1 55 | comics = load_comics(page) 56 | return result 57 | 58 | 59 | def load_chapters(page, comic_id): 60 | response = requests.get("http://www.ishuhui.net/ComicBooks/GetChapterList", 61 | params={"PageIndex": page, "id": comic_id}) 62 | return response.json()['Return']['List'] 63 | 64 | 65 | def refresh_chapters(listener=None): 66 | comics = data.get_comics() 67 | results = [] 68 | total = len(comics) 69 | current = 0 70 | for comic in comics: 71 | comic_id, saved_chapter_num = refresh_chapter(comic.id) 72 | current += 1 73 | result = {'comic_id': comic_id, 'count': saved_chapter_num} 74 | results.append(result) 75 | if listener is not None: 76 | listener(current, total, result) 77 | 78 | return results 79 | 80 | 81 | def refresh_comic_images(): 82 | comics = data.get_comics() 83 | result = dict() 84 | for comic in comics: 85 | front_cover = comic.front_cover 86 | if 'ishuhui' not in front_cover: 87 | current_app.logger.info('comic {} already refreshed'.format(comic.id)) 88 | continue 89 | image = requests.get(front_cover).content 90 | files = {'smfile': image} 91 | response = json.loads( 92 | requests.post('https://sm.ms/api/upload', files=files).text) 93 | if response.get('code') == 'success': 94 | url = response.get('data')['url'] 95 | comic.front_cover = url 96 | db.session.commit() 97 | result[comic.id] = url 98 | current_app.logger.info('refresh comic {} cover succeed, url :{}'.format( 99 | comic.id, url)) 100 | else: 101 | current_app.logger.info('failed comic {}'.format(comic.id)) 102 | return result 103 | 104 | 105 | def refresh_chapter(comic_id): 106 | page = 0 107 | chapters = load_chapters(page, comic_id) 108 | saved_chapter_num = 0 109 | while len(chapters) > 0: 110 | for chapter in chapters: 111 | try: 112 | database_chapter = Chapter.query.get(chapter['Id']) 113 | if database_chapter: 114 | same_chapter_number_chapters = Chapter.query.filter( 115 | and_(Chapter.comic_id == database_chapter.comic_id, 116 | Chapter.chapter_number == database_chapter.chapter_number)).all() 117 | for same_chapter_number_chapter in same_chapter_number_chapters: 118 | if same_chapter_number_chapter.id != database_chapter.id: 119 | # delete duplicate chapters 120 | db.session.delete(same_chapter_number_chapter) 121 | continue 122 | new_chapter = Chapter() 123 | new_chapter.id = chapter['Id'] 124 | new_chapter.title = chapter['Title'] 125 | new_chapter.comic_id = comic_id 126 | new_chapter.chapter_number = chapter['ChapterNo'] 127 | new_chapter.front_cover = chapter['FrontCover'] 128 | new_chapter.refresh_time = parse_date(chapter['RefreshTime']) 129 | db.session.add(new_chapter) 130 | saved_chapter_num += 1 131 | except Exception as e: 132 | current_app.logger.warning(e) 133 | page += 1 134 | chapters = load_chapters(page, comic_id) 135 | db.session.commit() 136 | current_app.logger.info('saved {} chapters of comic {}'.format(saved_chapter_num, comic_id)) 137 | return comic_id, saved_chapter_num 138 | -------------------------------------------------------------------------------- /ishuhui/templates/base/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | {% block title %}{% endblock %} 漫画 9 | 10 | 11 | 12 | 13 | 14 | 24 | {% endblock %} 25 | 26 | 27 | 28 |
29 | {% block content %}{% endblock %} 30 |
31 | {% block footer %} 32 |
© Copyright 2017 By lufficc.
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% endblock %} 43 | 44 | -------------------------------------------------------------------------------- /ishuhui/templates/base/msg.html: -------------------------------------------------------------------------------- 1 | {% with messages = get_flashed_messages(with_categories=true) %} 2 | {% if messages %} 3 | {% set (category, message) = messages|first %} 4 | {% set category = 'info' if category == 'message' else category %} 5 | 11 | {% endif %} 12 | {% endwith %} -------------------------------------------------------------------------------- /ishuhui/templates/base/navbar.html: -------------------------------------------------------------------------------- 1 | {% set classify_id = request.args.get('classify_id') %} 2 | -------------------------------------------------------------------------------- /ishuhui/templates/chapters.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% block title %}{{ comic.title }}{% endblock %} 3 | {% block content %} 4 |
5 | 6 | {% with transparent = True %} 7 | {% include 'base/navbar.html' %} 8 | {% endwith %} 9 |
10 |

{{ comic.title }}

11 |

{{ comic.description }}

12 |
13 |
14 |
15 | 17 |
18 | {% for chapter in chapters %} 19 |
20 |
21 |
22 |
{{ chapter.title }}
23 |
24 |

25 | 第{{ chapter.chapter_number }}话 26 |

27 | View 29 |
30 |
31 |
32 |
33 | {% endfor %} 34 |
35 |
36 | {% endblock %} 37 | {% block footer %} 38 | {{ super() }} 39 | 49 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/comics.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% set classify_id = request.args.get('classify_id') %} 3 | {% block title %} 4 | {% if classify_id == '2' %} 5 | 国产 6 | {% elif classify_id == '3' %} 7 | 鼠绘 8 | {% elif classify_id == '4' %} 9 | 热血 10 | {% elif classify_id == None %} 11 | 所有 12 | {% endif %} 13 | {% endblock %} 14 | {% block content %} 15 | {% include 'base/navbar.html' %} 16 |
17 |
18 | 20 |
21 | {% for comic in comics %} 22 |
23 |
24 | 25 |
26 |

{{ comic.title }}

27 |

{{ comic.description }} 28 | {% if comic.author %} 29 | -- {{ comic.author }} 30 | {% endif %} 31 |

32 |
33 |

34 | {{ comic.refresh_time }} 35 |

36 | View 38 |
39 |
40 |
41 |
42 | {% endfor %} 43 |
44 |
45 |
46 | {% endblock %} {% block footer %} {{ super() }} 47 | 61 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} {% block content %} 2 |
3 | 10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/images.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% block head %} 3 | {{ super() }} 4 | 5 | {% endblock %} 6 | {% block title %}{{ comic.title }}{{ chapter.chapter_number }}-{{ chapter.title }}{% endblock %} 7 | {% block content %} 8 |
9 |
10 | 16 | {% include 'base/msg.html' %} 17 |
18 |
19 | {% if prev_chapter %} 20 | Prev({{ prev_chapter.title }}) 22 | {% endif %} 23 | {% if next_chapter %} 24 | Next({{ next_chapter.title }}) 26 | {% endif %} 27 |
28 |
29 | {% for image in images %} 30 | 31 | 32 | 33 | {% endfor %} 34 |
35 | 45 |
46 | 47 | 48 |
49 |
50 | {% endblock %} 51 | {% block footer %} 52 | {{ super() }} 53 | 54 | 67 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/latest.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% block title %}最新{% endblock %} 3 | {% block content %} 4 | {% include 'base/navbar.html' %} 5 |
6 |
7 |
8 | {% for chapter in chapters %} 9 | {% set comic = chapter.comic() %} 10 |
11 |
13 |
14 |

{{ comic.title }}{{ chapter.chapter_number }}话

15 |
{{ chapter.title }}
16 |

{{ comic.description }}

17 |

18 | {{ chapter.refresh_time }} 19 |

20 | View 22 |
23 |
24 |
25 | {% endfor %} 26 |
27 |
28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block content %} 4 |
5 |
6 | {% include 'base/msg.html' %} 7 |
8 |
9 |

Login

10 |
11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 | Go home 22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/templates/mange.html: -------------------------------------------------------------------------------- 1 | {% extends "base/layout.html" %} 2 | {% block title %}Mange{% endblock %} 3 | {% block content %} 4 | {% include 'base/navbar.html' %} 5 | {% include 'base/msg.html' %} 6 |
7 |
8 |
9 |

{{ comic_count }} Comics

10 |
11 |
12 |
13 |
14 |

{{ chapter_count }} Chapters

15 |
16 |
17 |
18 |
19 |
20 |
21 |

Refresh Comics

22 |

Refresh all comics to keep update.

23 | 24 |
25 |
26 |
27 | {% if enable_celery %} 28 |
29 |
30 |
31 | {% endif %} 32 |
33 |

Refresh Chapters

34 |

Refresh all chapters to keep update.

35 | {% if enable_celery %} 36 | 37 | {% else %} 38 | 39 | {% endif %} 40 |
41 |
42 |
43 | 52 | {% endblock %} 53 | {% block footer %} 54 | {{ super() }} 55 | 125 | {% endblock %} -------------------------------------------------------------------------------- /ishuhui/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from ishuhui import create_app 2 | 3 | app = create_app('env') 4 | if __name__ == '__main__': 5 | app.run(host=app.config['HOST'], port=int(app.config['PORT']), debug=app.config['DEBUG']) 6 | --------------------------------------------------------------------------------