├── .dockerignore ├── .flake8 ├── .gitignore ├── Dockerfile ├── LICENSE ├── app_configs_tmpl.py ├── biligank_flask ├── __init__.py ├── jinja_filters.py ├── kvdb.py ├── logger.py ├── logging.py ├── main.py ├── mongodb.py ├── templates │ ├── 404.html │ ├── _maintain │ │ ├── QA.tmpl.html │ │ └── live_index_notice.tmpl.html │ ├── about.html │ ├── base.html │ ├── feedback.tmpl.html │ ├── live │ │ ├── ablive_dm.tmpl.html │ │ ├── ablive_en.tmpl.html │ │ ├── ablive_gf.tmpl.html │ │ ├── ablive_sc.tmpl.html │ │ ├── index.html │ │ ├── pagination.html │ │ └── tp.tmpl.html │ └── macro.html ├── utils.py └── views │ ├── __init__.py │ ├── general.py │ └── live │ ├── __init__.py │ ├── ablive_searcher.py │ ├── livedm_searcher.py │ ├── liveroom_searcher.py │ └── view.py ├── dev-2.1.flaskenv ├── dev-2.2.flaskenv ├── mypy.ini ├── requirements.txt ├── run.bat └── serve.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | **/__pycache__* 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E251 4 | exclude = 5 | .*, 6 | __pycache__, 7 | utils.py, 8 | __init__.py, 9 | app_configs*, 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | 148 | .flaskenv 149 | 150 | app_configs.py 151 | logs/* 152 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | COPY requirements.txt /tmp/requirements.txt 4 | RUN pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 5 | 6 | WORKDIR /app 7 | VOLUME /app/logs 8 | 9 | COPY biligank_flask biligank_flask/ 10 | 11 | COPY serve.sh app_configs.py ./ 12 | RUN chmod +x ./serve.sh 13 | 14 | EXPOSE 7771 15 | 16 | CMD ["bash", "./serve.sh"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Grvzard 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 | -------------------------------------------------------------------------------- /app_configs_tmpl.py: -------------------------------------------------------------------------------- 1 | 2 | class Config: 3 | ERROR_TEXT = "出错了。" 4 | ABLIVE = { 5 | "MONGO_CONFIG": "mongodb://localhost:27017/", 6 | "ROADS": ["ablive_dm", "ablive_en", "ablive_gf", "ablive_sc", "tp"], 7 | "LIMITS": { 8 | 'ablive_dm': 2, 9 | 'ablive_en': 5, 10 | 'ablive_gf': 2, 11 | 'ablive_sc': 1, 12 | 'tp': 5, 13 | 'livedm': 9, 14 | }, 15 | } 16 | # https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/ 17 | SQLALCHEMY_TRACK_MODIFICATIONS = False 18 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://localhost:3306/tp" 19 | SQLALCHEMY_BINDS = { 20 | "ablive_dm": "mysql+pymysql://localhost:3306/ablive_dm", 21 | "ablive_en": "mysql+pymysql://localhost:3306/ablive_en", 22 | "ablive_gf": "mysql+pymysql://localhost:3306/ablive_gf", 23 | "ablive_sc": "mysql+pymysql://localhost:3306/ablive_sc", 24 | "tp": "mysql+pymysql://localhost:3306/tp", 25 | } 26 | SEARCH_LOGGER = { 27 | "json": "search_log.json", 28 | } 29 | FEEDBACK_LOGGER = { 30 | "json": "feedback.json", 31 | } 32 | KV_DB = { 33 | "config": "", 34 | } 35 | -------------------------------------------------------------------------------- /biligank_flask/__init__.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey 2 | 3 | monkey.patch_all() 4 | 5 | from .main import app 6 | -------------------------------------------------------------------------------- /biligank_flask/jinja_filters.py: -------------------------------------------------------------------------------- 1 | 2 | from .utils import ts2clock, ts2date, ts2date_2 3 | 4 | 5 | def register_jinja_filters(app): 6 | app.jinja_env.filters['strftime'] = ts2date 7 | app.jinja_env.filters['strftime_2'] = ts2date_2 8 | app.jinja_env.filters['ts2clock'] = ts2clock 9 | -------------------------------------------------------------------------------- /biligank_flask/kvdb.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import pymongo 4 | 5 | 6 | class KvDb: 7 | def __init__(self, app = None): 8 | if app is not None: 9 | self.init_app(app) 10 | 11 | def init_app(self, app) -> None: 12 | config = app.config['KV_DB'] and app.config['KV_DB']['config'] 13 | if config: 14 | client = pymongo.MongoClient(config) 15 | self.coll = client['biligank_web']['var'] 16 | 17 | self._available = bool(config) 18 | 19 | app.extensions['kvdb'] = self 20 | 21 | def get(self, key: str) -> Any: 22 | if not self._available: 23 | return None 24 | 25 | var = self.coll.find_one({'key': key}) 26 | if var: 27 | return var['value'] 28 | else: 29 | return None 30 | 31 | def set(self, key: str, value: Any) -> None: 32 | ... 33 | -------------------------------------------------------------------------------- /biligank_flask/logger.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Final, Dict 4 | 5 | import pymongo 6 | import httpx 7 | from pydantic import BaseModel 8 | 9 | from .utils import get_date 10 | 11 | __all__ = 'MultiLogger', 'JsonLogger', 'MongoLogger', 'TgbotLogger' 12 | 13 | 14 | class MultiLogger: 15 | def __init__(self, **loggers): 16 | self.loggers = [] 17 | for type_, setting in loggers.items(): 18 | if type_.startswith('mongo'): 19 | h = MongoLogger(setting) 20 | elif type_.startswith('json'): 21 | h = JsonLogger(setting) 22 | elif type_.startswith('tgbot'): 23 | h = TgbotLogger(setting) 24 | else: 25 | raise Exception('unknown logger type') 26 | self.loggers.append(h) 27 | 28 | def log(self, log_info): 29 | for logger in self.loggers: 30 | logger.log(log_info) 31 | 32 | 33 | class JsonLogger: 34 | def __init__(self, file_name): 35 | Path('logs').mkdir(exist_ok=True) 36 | self.file = Path('logs') / file_name 37 | 38 | def log(self, log_info): 39 | with self.file.open('a', encoding='utf-8') as f: 40 | json.dump(log_info, f, ensure_ascii=False, indent=4) 41 | f.write(',') 42 | 43 | 44 | class MongoLogger: 45 | def __init__(self, setting): 46 | self.mongo_client: pymongo.MongoClient = pymongo.MongoClient(setting['config']) 47 | self.db = self.mongo_client[setting['db']] 48 | 49 | def log(self, log_info): 50 | self.db[get_date()].insert_one(log_info) 51 | 52 | 53 | class TgbotConfig(BaseModel): 54 | token: str 55 | chat_id: str 56 | tag_prefix: str = 'biligank' 57 | proxy: str = '' 58 | 59 | 60 | class TgbotLogger: 61 | HTML_ENTITIES: Final[Dict[str, str]] = { 62 | '<': '<', 63 | '>': '>', 64 | '&': '&', 65 | } 66 | 67 | def __init__(self, setting: dict): 68 | self._config = TgbotConfig(**setting) 69 | self._proxies = {} 70 | if self._config.proxy: 71 | self._proxies['https://'] = self._config.proxy 72 | 73 | def log(self, log_info): 74 | url = f"https://api.telegram.org/bot{self._config.token}/sendMessage" 75 | 76 | word_list = [] 77 | for info in log_info.values(): 78 | i = str(info) 79 | for c in i: 80 | if (r := self.HTML_ENTITIES.get(c, None)): 81 | word_list.append(r) 82 | else: 83 | word_list.append(c) 84 | word_list.append('\n') 85 | 86 | text = f"#{self._config.tag_prefix}\n" + ''.join(word_list) 87 | 88 | payload = { 89 | "chat_id": self._config.chat_id, 90 | "text": text, 91 | "parse_mode": "HTML", 92 | 'disable_web_page_preview': True, 93 | } 94 | 95 | resp = httpx.post(url, json=payload, timeout=10, proxies=self._proxies).json() 96 | 97 | if resp['ok']: 98 | return True 99 | else: 100 | raise Exception( 101 | 'tgbot send msg failed: [%s] %s' 102 | % (resp['result']['error_code'], resp['result']['description']) 103 | ) 104 | -------------------------------------------------------------------------------- /biligank_flask/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from flask import has_request_context, request 5 | 6 | 7 | class RequestFormatter(logging.Formatter): 8 | def format(self, record): 9 | if has_request_context(): 10 | record.path = request.full_path 11 | record.remote_addr = request.remote_addr 12 | else: 13 | record.path = None 14 | record.remote_addr = None 15 | 16 | return super().format(record) 17 | 18 | 19 | def configure_logging(app): 20 | Path('logs').mkdir(exist_ok=True) 21 | fmt = RequestFormatter( 22 | '[%(asctime)s] [%(remote_addr)s] %(path)s\n -- %(message)s' 23 | ) 24 | error_handler = logging.FileHandler('logs/error.log') 25 | error_handler.setLevel(logging.ERROR) 26 | error_handler.setFormatter(fmt) 27 | 28 | app.logger.addHandler(error_handler) 29 | -------------------------------------------------------------------------------- /biligank_flask/main.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask, render_template, current_app 3 | 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | if app.debug: 9 | app.config.from_object('app_configs.DevConfig') 10 | else: 11 | app.config.from_object('app_configs.ProdConfig') 12 | 13 | from flask_sqlalchemy import SQLAlchemy 14 | from .mongodb import MongoDB 15 | from .kvdb import KvDb 16 | sqldb = SQLAlchemy(app) 17 | mongodb = MongoDB(app) 18 | kvdb = KvDb(app) 19 | 20 | with app.app_context(): 21 | from .views import general, live 22 | app.register_blueprint(general.bp) 23 | app.register_blueprint(live.bp) 24 | 25 | from .jinja_filters import register_jinja_filters 26 | register_jinja_filters(app) 27 | 28 | from .logging import configure_logging 29 | configure_logging(app) 30 | 31 | 32 | @app.errorhandler(Exception) 33 | def default_error(e): 34 | current_app.logger.error(str(e)) 35 | return { 36 | 'html': current_app.config['ERROR_TEXT'] 37 | } 38 | 39 | 40 | @app.errorhandler(404) 41 | def page_notfound(error): 42 | return render_template('404.html'), 404 43 | -------------------------------------------------------------------------------- /biligank_flask/mongodb.py: -------------------------------------------------------------------------------- 1 | 2 | import pymongo 3 | from flask import Flask 4 | 5 | 6 | class MongoDB: 7 | client = None 8 | 9 | def __init__(self, app: Flask = None): 10 | if app is not None: 11 | self.init_app(app) 12 | 13 | def init_app(self, app) -> None: 14 | config = app.config['ABLIVE']['MONGO_CONFIG'] 15 | self.client = pymongo.MongoClient(config) 16 | 17 | app.extensions['mongo_client'] = self.client 18 | -------------------------------------------------------------------------------- /biligank_flask/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | 404 Page Not Found 6 |
7 | {% endblock content %} 8 | -------------------------------------------------------------------------------- /biligank_flask/templates/_maintain/QA.tmpl.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grvzard/biligank-flask/44248ee56fae138d705374e77cc66b8af921323c/biligank_flask/templates/_maintain/QA.tmpl.html -------------------------------------------------------------------------------- /biligank_flask/templates/_maintain/live_index_notice.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import card%} 2 | 3 | {{ card( 4 | 'Github: biligank web flask' |safe, 5 | '',) 6 | }} 7 | -------------------------------------------------------------------------------- /biligank_flask/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import "macro.html" as m %} 3 | 4 | 5 | {% macro log(date, content, checked='') -%} 6 |
7 | 8 | 10 |
11 | {%- endmacro %} 12 | 13 | 14 | {% block content %} 15 | 16 | 17 | 103 | 104 | {{ m.card("有问题会在这里解释👇") }} 105 | {% include "_maintain/QA.tmpl.html" %} 106 |
107 | 108 |
109 | 110 | {# include "donate.html" #} 111 | 112 | 113 | {% endblock %} 114 | -------------------------------------------------------------------------------- /biligank_flask/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | biligank 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% block scripts %}{% endblock %} 22 | 23 | 24 | 25 |
26 | {% block navbar %} 27 | 42 | {% endblock navbar %} 43 |
44 | 45 |
46 | {% block content %} 47 | {% endblock content %} 48 |
49 | 50 | {% block footer %} 51 |
52 |
53 |
54 |
55 | {% endblock footer %} 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /biligank_flask/templates/feedback.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 |
20 | 36 |
37 | 38 | 65 | -------------------------------------------------------------------------------- /biligank_flask/templates/live/ablive_dm.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import space %} 2 | 3 | 4 | {% block result %} 5 | 6 | 7 | {% block danmaku_filter %} 8 | {# if first_time #} 9 | {% if False %} 10 |
11 |
12 | 15 | 16 |
17 | 18 | 19 |
20 |
21 |
22 | 23 | 41 | {% endif %} 42 | {% endblock danmaku_filter %} 43 | 44 | 45 | {% for card in data %} 46 | {% set date, liverid, danmakus = card.values() %} 47 | {% set liver_uname = rooms_dict[liverid].uname %} 48 |
49 |
50 | {{date}} {{ space(liverid, liver_uname) }} 51 | {{card.danmakus | length}} 52 |
53 |
54 | {% for danmaku in danmakus %} 55 | 56 | {{ danmaku[0] |ts2clock }} -> {{ danmaku[1] }}
57 |
58 | {% endfor %} 59 |
60 |
61 | {% endfor %} 62 | 63 | 64 | {% include "live/pagination.html" %} 65 | 66 | 67 | {##} 82 | 83 | 84 | {% endblock result %} 85 | -------------------------------------------------------------------------------- /biligank_flask/templates/live/ablive_en.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import space %} 2 | 3 | 4 | {% block result %} 5 | 6 | 7 | {% if first_time %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
时间主播分区
18 | {% endif %} 19 | 20 | 21 | 22 | {% for entry in data %} 23 | {% set room_info = rooms_dict[entry.liverid] %} 24 | 25 | 26 | 27 | 28 | 29 | {% endfor %} 30 | 31 |
{{ entry.ts |strftime }}{{ space(entry.liverid, room_info.uname) }}{{ room_info.area_name }}
32 | 33 | 38 | 39 | {% include "live/pagination.html" %} 40 | 41 | 42 | {% endblock result %} 43 | -------------------------------------------------------------------------------- /biligank_flask/templates/live/ablive_gf.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import space %} 2 | 3 | 4 | {% block result %} 5 | 6 | 7 | {% if first_time %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
时间主播礼物花费(¥)
19 | {% endif %} 20 | 21 | 22 | 23 | {% for gift in data %} 24 | {% set liver_uname = rooms_dict[gift.liverid].uname %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 | 33 |
{{ gift.ts |strftime }}{{ space(gift.liverid, liver_uname) }}{{ gift.gift_info }}{{ gift.gift_cost }}
34 | 35 | 40 | 41 | {% include "live/pagination.html" %} 42 | 43 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /biligank_flask/templates/live/ablive_sc.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import space %} 2 | 3 | 4 | {% block result %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for superchat in data %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
{{next_offset}}
时间用户花费(¥)留言
{{ superchat.ts |ts2clock }}{{ space(superchat.uid, superchat.uname) }}{{ superchat.sc_price |int }}{{ superchat.text }}
28 | 29 | 36 | 37 | 38 | {% include "live/pagination.html" %} 39 | 40 | 41 | {% endblock result %} -------------------------------------------------------------------------------- /biligank_flask/templates/live/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'macro.html' as m %} 3 | 4 | 5 | {% block content %} 6 | 7 | {% block search_bar %} 8 | 27 | 28 | 79 | 80 | {% endblock search_bar %} 81 | 82 | {% block result %} 83 |
84 | {% for info in notice %} 85 |
86 | 87 | {{info['date']}} 88 | {{info['content']}} 89 |
90 | {% endfor %} 91 | 92 | {% include "_maintain/live_index_notice.tmpl.html" %} 93 | 94 |
95 |
96 |
97 | 98 | 99 | 100 | 103 | 104 | 105 | {% endblock result %} 106 | 107 | {% endblock %} 108 | 109 | 110 | -------------------------------------------------------------------------------- /biligank_flask/templates/live/pagination.html: -------------------------------------------------------------------------------- 1 |
2 | 18 | 19 | 29 | 30 |
-------------------------------------------------------------------------------- /biligank_flask/templates/live/tp.tmpl.html: -------------------------------------------------------------------------------- 1 | {% from "macro.html" import link %} 2 | 3 | 4 | {% block result %} 5 | 6 | {% if first_time %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
封面标题分区时间人气
18 | {% endif %} 19 | 20 | 21 | 22 | {% for tp in data %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% endfor %} 31 | 32 |
{{ link(tp.cover, '链接') }}{{ tp.title }}{{ tp.area_name }}{{ tp.c_ts |strftime_2 }} — {{ tp.last_update |strftime_2 }}{{ tp.watched_num }}
33 | 34 | 39 | 40 | 41 | {% include "live/pagination.html" %} 42 | 43 | {% endblock result %} -------------------------------------------------------------------------------- /biligank_flask/templates/macro.html: -------------------------------------------------------------------------------- 1 | {% macro card() -%} 2 |
3 |
4 | {% for text in varargs %} 5 | {{ text }}
6 | {% endfor %} 7 |
8 |
9 | {%- endmacro %} 10 | 11 | 12 | {% macro space(uid, uname) -%} 13 | {{ uname }} 14 | {%- endmacro %} 15 | 16 | 17 | {% macro link(site, display) -%} 18 | {{ display }} 19 | {%- endmacro %} 20 | -------------------------------------------------------------------------------- /biligank_flask/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import timeit 4 | 5 | 6 | def write_json(data_dict, filename): 7 | with open('./%s.json' %filename, 'a', encoding='utf-8') as f: 8 | json.dump(data_dict, f, ensure_ascii=False, indent=4) 9 | f.write(',') 10 | 11 | 12 | class Timer: 13 | __slots__ = ('st', 'et') 14 | 15 | def __enter__(self): 16 | self.tick() 17 | 18 | def __exit__(self, exc_type, exc_value, exc_tb): 19 | self.tock() 20 | 21 | def tick(self): 22 | self.st = timeit.default_timer() 23 | 24 | def tock(self): 25 | self.et = timeit.default_timer() 26 | 27 | @property 28 | def result(self): 29 | return round(self.et - self.st, 3) 30 | 31 | 32 | def get_date(): 33 | return time.strftime("%Y_%m_%d", time.localtime()) 34 | 35 | def get_clock(): 36 | return time.strftime("%H:%M:%S", time.localtime()) 37 | 38 | def ts2date(time_stamp): 39 | struct_time = time.localtime(time_stamp) 40 | return time.strftime("%Y-%m-%d %H:%M:%S", struct_time) 41 | 42 | def ts2date_2(time_stamp): 43 | struct_time = time.localtime(time_stamp) 44 | return time.strftime("%m-%d %H:%M", struct_time) 45 | 46 | def ts2clock(time_stamp): 47 | struct_time = time.localtime(time_stamp) 48 | return time.strftime("%H:%M:%S", struct_time) 49 | 50 | def get_time(): 51 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 52 | -------------------------------------------------------------------------------- /biligank_flask/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grvzard/biligank-flask/44248ee56fae138d705374e77cc66b8af921323c/biligank_flask/views/__init__.py -------------------------------------------------------------------------------- /biligank_flask/views/general.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, current_app, render_template, request 3 | 4 | from ..logger import MultiLogger 5 | from ..utils import get_time 6 | from . import live 7 | 8 | bp = Blueprint( 9 | name='general', 10 | import_name=__name__, 11 | ) 12 | 13 | 14 | bp.add_url_rule( 15 | rule='/', 16 | view_func=live.index, 17 | ) 18 | 19 | 20 | feedback_logger = MultiLogger(**current_app.config['FEEDBACK_LOGGER']) 21 | 22 | 23 | @bp.route('/feedback', methods=['POST']) 24 | def feedback(): 25 | content_type = request.headers.get("content_type") 26 | assert content_type is not None 27 | if content_type == 'application/json': 28 | assert request.json is not None 29 | text = request.json.get('text') 30 | elif content_type.startswith('application/x-www-form-urlencoded'): 31 | text = request.form['text'] 32 | else: 33 | raise Exception('unknown content type') 34 | 35 | data = { 36 | 'time': get_time(), 37 | 'text': text, 38 | 'ip': request.headers.get('x-real-ip'), 39 | } 40 | feedback_logger.log(data) 41 | return 'ok' 42 | 43 | 44 | @bp.route('/about') 45 | def about(): 46 | return render_template('about.html', status={}) 47 | 48 | 49 | @bp.route('/metrics') 50 | def metrics(): 51 | kvdb = current_app.extensions['kvdb'] 52 | return kvdb.get('status') or {} 53 | -------------------------------------------------------------------------------- /biligank_flask/views/live/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, current_app, render_template 3 | 4 | from ...logger import MultiLogger 5 | from .ablive_searcher import AbliveSearcher 6 | from .livedm_searcher import LivedmSearcher 7 | from .view import AbliveView 8 | 9 | __all__ = 'bp', 10 | 11 | 12 | bp = Blueprint( 13 | name='live', 14 | import_name=__name__, 15 | url_prefix='/live', 16 | ) 17 | 18 | ROADS = current_app.config['ABLIVE']['ROADS'] 19 | LIMITS = current_app.config['ABLIVE']['LIMITS'] 20 | 21 | search_logger = MultiLogger( 22 | **current_app.config['SEARCH_LOGGER'] 23 | ) 24 | 25 | for road in ROADS: 26 | if road == 'livedm': 27 | searcher = LivedmSearcher( 28 | limits=LIMITS['livedm'], 29 | ) 30 | else: 31 | searcher = AbliveSearcher( 32 | road=f'{road}', 33 | limits=LIMITS[f'{road}'], 34 | ) 35 | 36 | bp.add_url_rule( 37 | rule=f"/{road}", 38 | view_func=AbliveView.as_view( 39 | name=f'{road}', 40 | road=f'{road}', 41 | searcher=searcher, 42 | search_logger=search_logger, 43 | ), 44 | ) 45 | 46 | 47 | @bp.route('/') 48 | def index(): 49 | kvdb = current_app.extensions['kvdb'] 50 | notice = kvdb.get('notice') or [] 51 | return render_template( 52 | 'live/index.html', 53 | notice=notice, 54 | ) 55 | 56 | 57 | @bp.route('/data') 58 | def data(): 59 | raise Exception('deprecated route') 60 | -------------------------------------------------------------------------------- /biligank_flask/views/live/ablive_searcher.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Any, Optional 3 | 4 | from flask import current_app 5 | 6 | from ...utils import get_date 7 | 8 | __all__ = 'AbliveSearcher', 9 | 10 | 11 | DailyAbliveData = tuple[list[dict[str, Any]], set[Any]] 12 | 13 | 14 | class AbliveSearcher: 15 | def __init__(self, road: str, limits: int) -> None: 16 | self.road = road 17 | self.limits = limits 18 | self.tables: list = [] 19 | self.last_table = '' 20 | 21 | def update_tables(self, db) -> None: 22 | # error: get_bind() got an unexpected keyword argument 'bind' 23 | # https://github.com/pallets-eco/flask-sqlalchemy/issues/953 24 | # resolved in flask-sqlalchemy 3.0.0 released on 2022.10.04 25 | rs_tup = db.session.execute( 26 | 'show tables;', 27 | bind=db.get_engine(bind_key=f'{self.road}') 28 | ).all() 29 | tables = [rs[0] for rs in rs_tup] 30 | tables.sort(reverse=False) 31 | if self.road in ('tp',): 32 | self.tables = tables 33 | else: 34 | self.tables = tables[-7:] 35 | self.last_table = tables[-1] 36 | 37 | def more(self, uid: int, offset: str) -> tuple[list[Optional[Any]], str, bool, set[Optional[int]]]: # noqa 38 | if uid <= 0: 39 | raise Exception(f"invalid uid: {uid}") 40 | road = self.road 41 | db = current_app.extensions['sqlalchemy'] 42 | 43 | if get_date() != self.last_table or not self.tables: 44 | self.update_tables(db) 45 | 46 | try: 47 | if offset == '0': 48 | offset = self.last_table 49 | table_idx = self.tables.index(offset) + 1 50 | else: 51 | table_idx = self.tables.index(offset) 52 | except Exception as e: 53 | raise e 54 | 55 | data = [] 56 | liverids = set() 57 | while table_idx > 0: 58 | table_idx -= 1 59 | table = self.tables[table_idx] 60 | 61 | f = getattr(self, f'daily_{road}') 62 | _data, _liverids = f(db, table, uid) 63 | data.extend(_data) 64 | liverids |= _liverids 65 | 66 | if len(data) >= self.limits: 67 | break 68 | 69 | db.session.commit() 70 | 71 | next_offset = table 72 | has_more = False if table_idx == 0 else True 73 | 74 | return data, next_offset, has_more, liverids 75 | 76 | def daily_tp(self, db, table: str, uid: int) -> DailyAbliveData: 77 | date_tp_list = [] 78 | 79 | rs_tup = db.session.execute( 80 | f'select title, c_ts, last_update, watched_num, area_name, cover from `{table}` where uid = :uid ORDER BY _id DESC', # noqa 81 | {'uid': uid}, 82 | bind=db.get_engine(bind_key='tp') 83 | ).all() 84 | for tp in rs_tup: 85 | date_tp_list.append(tp) 86 | 87 | return date_tp_list, set() 88 | 89 | def daily_ablive_dm(self, db, table: str, uid: int) -> DailyAbliveData: 90 | date_danmaku_cards = [] 91 | _livers = set() 92 | 93 | rs_tup = db.session.execute( 94 | f'select ts, liverid, text from `{table}` where uid = :uid ORDER BY _id DESC', # noqa 95 | {'uid': uid}, 96 | bind=db.get_engine(bind_key='ablive_dm') 97 | ).all() 98 | 99 | last_liverid = 0 100 | for ts, liverid, text in rs_tup: 101 | if liverid != last_liverid: 102 | last_liverid = liverid 103 | _livers.add(liverid) 104 | card = { 105 | 'date': table, 106 | 'liverid': liverid, 107 | 'danmakus': [], 108 | } 109 | date_danmaku_cards.append(card) 110 | date_danmaku_cards[-1]['danmakus'].append((ts, text)) 111 | 112 | return date_danmaku_cards, _livers 113 | 114 | def daily_ablive_en(self, db, table: str, uid: int) -> DailyAbliveData: 115 | date_entry_list = [] 116 | _livers = set() 117 | 118 | rs_tup = db.session.execute( 119 | f'select ts, liverid from `{table}` where uid = :uid ORDER BY _id DESC', # noqa 120 | {'uid': uid}, 121 | bind=db.get_engine(bind_key='ablive_en') 122 | ).all() 123 | for entry in rs_tup: 124 | date_entry_list.append(dict(entry)) 125 | _livers.add(entry['liverid']) 126 | 127 | return date_entry_list, _livers 128 | 129 | def daily_ablive_gf(self, db, table: str, uid: int) -> DailyAbliveData: 130 | date_gift_list = [] 131 | _livers = set() 132 | 133 | rs_tup = db.session.execute( 134 | f'select ts, liverid, gift_info, gift_cost from `{table}` where uid = :uid ORDER BY _id DESC', # noqa 135 | {'uid': uid}, 136 | bind=db.get_engine(bind_key='ablive_gf') 137 | ).all() 138 | for gift in rs_tup: 139 | date_gift_list.append(dict(gift)) 140 | _livers.add(gift['liverid']) 141 | 142 | return date_gift_list, _livers 143 | 144 | def daily_ablive_sc(self, db, table: str, uid: int) -> DailyAbliveData: 145 | date_sc_list = [] 146 | liverid = uid 147 | 148 | rs_tup = db.session.execute( 149 | f'select ts, uid, uname, text, sc_price from `{table}` where liverid = :liverid ORDER BY _id DESC', # noqa 150 | {'liverid': liverid}, 151 | bind=db.get_engine(bind_key='ablive_sc') 152 | ).all() 153 | for superchat in rs_tup: 154 | date_sc_list.append(dict(superchat)) 155 | 156 | return date_sc_list, set() 157 | -------------------------------------------------------------------------------- /biligank_flask/views/live/livedm_searcher.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from flask import current_app 4 | 5 | 6 | class LivedmSearcher: 7 | def __init__(self, limits: int) -> None: 8 | self.limits = limits 9 | self.update_colls(current_app.extensions['mongo_client']['livedm']) 10 | 11 | def update_colls(self, db) -> None: 12 | colls = db.list_collection_names() 13 | colls.sort(reverse=False) 14 | self.colls = colls 15 | self.last_coll = colls[-1] 16 | 17 | def more(self, uid: int, offset: str) -> tuple[Optional[list[Any]], str, bool, set[Optional[int]]]: # noqa 18 | db = current_app.extensions['mongo_client']['livedm'] 19 | 20 | try: 21 | if offset == '0': 22 | offset = self.last_coll 23 | table_idx = self.colls.index(offset) + 1 24 | else: 25 | table_idx = self.colls.index(offset) 26 | except Exception as e: 27 | raise e 28 | 29 | data = [] 30 | liverids = set() 31 | while table_idx > 0: 32 | table_idx -= 1 33 | table = self.colls[table_idx] 34 | 35 | _data, _liverids = self.daily_docs(db, table, uid) 36 | data.extend(_data) 37 | liverids |= _liverids 38 | 39 | if len(data) >= self.limits: 40 | break 41 | 42 | next_offset = table 43 | has_more = False if table_idx == 0 else True 44 | 45 | return data, next_offset, has_more, liverids 46 | 47 | def daily_docs(self, db, date: str, uid: int) -> tuple[list, set]: 48 | docs = db[date].find({ 49 | 'uid': uid, 50 | }, { 51 | '_id': 0, 52 | }, 53 | ) 54 | date_dm_cards = [] 55 | _liverids = set() 56 | for doc in docs: 57 | doc['dm'].sort(reverse=False) 58 | 59 | dm_card = { 60 | 'date': date, 61 | 'liverid': doc['liverid'], 62 | 'danmakus': doc['dm'], 63 | } 64 | _liverids.add(doc['liverid']) 65 | date_dm_cards.append(dm_card) 66 | 67 | return date_dm_cards, _liverids 68 | 69 | def get_doc(self, uid: int, liverid: int, date: str) -> Optional[dict[str, Any]]: 70 | part = date[:7] 71 | doc = self.mongo_client[f'livedm_{part}'][date].find_one({ 72 | 'uid': uid, 73 | 'liverid': int(liverid), 74 | }, { 75 | '_id': 0, 76 | }, 77 | ) 78 | if doc: 79 | doc['dm'].sort(reverse=False) 80 | 81 | return doc 82 | -------------------------------------------------------------------------------- /biligank_flask/views/live/liveroom_searcher.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import Union 3 | 4 | 5 | def get_livers_info(db, liverids: Sequence[int]): 6 | rooms_dict = {} 7 | for coll in ('all', 'rooms_state'): 8 | rs = db[coll].find({ 9 | 'uid': {'$in': list(liverids)}, 10 | }, { 11 | 'uid': 1, 'uname': 1, 'area_name': 1, '_id': 0, 12 | }, 13 | ) 14 | for room_info in rs: 15 | liverid = int(room_info['uid']) 16 | rooms_dict[liverid] = room_info 17 | 18 | return rooms_dict 19 | 20 | 21 | def get_liver_info(db, liverid: Union[int, str]): 22 | for coll in ('all', 'rooms_state'): 23 | room_info = db[coll].find_one({ 24 | 'uid': int(liverid) 25 | }, { 26 | 'uname': 1, 'area_name': 1, '_id': 0 27 | }, 28 | ) 29 | if room_info: 30 | break 31 | else: 32 | room_info = { 33 | 'uname': 'bug', 34 | 'area_name': 'bug', 35 | } 36 | 37 | return room_info 38 | -------------------------------------------------------------------------------- /biligank_flask/views/live/view.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from flask import current_app, render_template, request 4 | from flask.views import View 5 | 6 | from ...utils import Timer 7 | from . import liveroom_searcher as liveroom 8 | 9 | __all__ = 'AbliveView', 10 | 11 | 12 | class AbliveView(View): 13 | init_every_request = False 14 | methods = ["GET"] 15 | 16 | def __init__(self, road, searcher, search_logger): 17 | self.road = road 18 | self.template = f'live/{road}.tmpl.html' 19 | if road == 'livedm': 20 | self.template = 'live/ablive_dm.tmpl.html' 21 | self.searcher = searcher 22 | self.search_logger = search_logger 23 | 24 | def dispatch_request(self): 25 | road = self.road 26 | uid = int(request.args.get('uid')) 27 | offset = request.args.get('offset') 28 | not_render = bool(request.args.get('not_render')) 29 | first_time = True if offset == '0' else False 30 | 31 | timer = Timer() 32 | with timer: 33 | data, next_offset, has_more, liverids = self.searcher.more(uid, offset) 34 | 35 | rooms_dict = liveroom.get_livers_info( 36 | db=current_app.extensions['mongo_client']['bili_liveroom'], 37 | liverids=liverids, 38 | ) 39 | 40 | self.search_logger.log({ 41 | 'uid': uid, 42 | 'road': road, 43 | 'offset': offset, 44 | 'loadtime': timer.result, 45 | 'ip': request.headers.get('x-real-ip'), 46 | 'not_render': not_render, 47 | 'ts': int(time.time()), 48 | }) 49 | 50 | resp = { 51 | "road": road, 52 | "uid": uid, 53 | "next_offset": next_offset, 54 | "first_time": first_time, 55 | "data": data, 56 | "rooms_dict": rooms_dict, 57 | "has_more": has_more, 58 | } 59 | 60 | if not_render: 61 | return resp 62 | 63 | return { 64 | "html": render_template(self.template, **resp) 65 | } 66 | -------------------------------------------------------------------------------- /dev-2.1.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP = biligank_flask:app 2 | FLASK_ENV = development -------------------------------------------------------------------------------- /dev-2.2.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP = biligank_flask:app 2 | FLASK_DEBUG = True -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disallow_untyped_defs = False 3 | ignore_missing_imports = True 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql == 1.0.2 2 | Flask >= 2.2.0, < 2.3.0 3 | Flask-SQLAlchemy == 3.0.0 4 | pymongo >= 4.2.0, < 4.3.0 5 | python-dotenv == 0.21.0 6 | gunicorn == 20.1.0 7 | gevent == 21.12.0 8 | pydantic >= 1.10.0, < 2.0.0 9 | httpx[socks] >= 0.24.0, < 1.0.0 10 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | python -m flask --app biligank_flask:app --debug run 2 | -------------------------------------------------------------------------------- /serve.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | exec gunicorn -w 2 -k gevent --bind 0.0.0.0:7771 biligank_flask:app 5 | --------------------------------------------------------------------------------