diff --git a/Dockerfile b/Dockerfile index 4bac01a..9be3bed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ COPY . /app ENV TZ Asia/Shanghai RUN pip install -r requirements.txt --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com EXPOSE 8182 +# 数据库初始化/升级 +RUN flask db upgrade # 启动waitress -#ENTRYPOINT ["python","app.py"] ENTRYPOINT ["waitress-serve","--port", "8182", "--call","app:create_app"] \ No newline at end of file diff --git a/README.md b/README.md index b9bb79d..c980f4c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ $ pip3 install -r requirements.txt $ export PANDORA_NEXT_DOMAIN=https://www.baidu.com # 修改以下路径为你本机PandoraNext的路径,确保路径中包含config.json $ export PANDORA_NEXT_PATH=/path/to/pandora +# 数据库初始化 +$ flask db upgrade # 启动 $ python3 waitress_run.py # 或者在后台启动 @@ -67,13 +69,14 @@ $ nohup python3 waitress_run.py & > }``` ## Todo - [x] 展示Pandora额度信息 -- [ ] 生成指定账号下各Share Token的用量情况柱状图 +- [x] 生成指定账号下各Share Token的用量情况柱状图 +- [x] 支持预置Token、Refresh Token - [ ] Русская адаптация - [ ] 支持管理Pool Token - [ ] 支持编辑 - [ ] 支持更多PandoraNext配置 - [ ] 支持更多验证码 -- [ ] ~~代码优化~~ +- [x] ~~代码优化~~ ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=nianhua99/PandoraNext-Helper&type=Date)](https://star-history.com/#nianhua99/PandoraNext-Helper&Date) diff --git a/app.py b/app.py index a843591..3ccc8d3 100644 --- a/app.py +++ b/app.py @@ -2,12 +2,12 @@ import os import re import secrets -import sqlite3 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore -from flask import Flask, g, redirect, url_for +from flask import Flask, redirect, url_for from flask_bootstrap import Bootstrap5 from flask_login import LoginManager +from flask_migrate import Migrate from flask_moment import Moment from flask_apscheduler import APScheduler from loguru import logger @@ -41,57 +41,6 @@ def context_api_prefix(): return dict(api_prefix=app.config['proxy_api_prefix']) -def init_db(): - with app.app_context(): - db = connect_db() - db.execute(''' - create table if not exists users - ( - id INTEGER - primary key autoincrement, - email TEXT not null, - password TEXT not null, - session_token TEXT, - access_token TEXT, - share_list TEXT, - create_time datetime, - update_time datetime, - shared INT default 0 - ) - ''') - db.commit() - - -def connect_db(): - db = getattr(g, '_database', None) - if db is None: - db = g._database = sqlite3.connect(os.path.join(app.config['pandora_path'], DATABASE)) - db.row_factory = sqlite3.Row - return db - - -def query_db(query, args=(), one=False): - # 查看g对象是否存在db属性,如果不存在则创建 - if not hasattr(g, 'db'): - g.db = connect_db() - cur = g.db.execute(query, args) - rv = [dict((cur.description[idx][0], value) - for idx, value in enumerate(row)) for row in cur.fetchall()] - return (rv[0] if rv else None) if one else rv - - -@app.before_request -def before_request(): - g.db = connect_db() - - -@app.after_request -def after_request(result): - if hasattr(g, 'db'): - g.db.close() - return result - - def check_require_config(): PANDORA_NEXT_PATH = os.getenv('PANDORA_NEXT_PATH') # 如果PANDORA_NEXT_PATH 为空则检查/data下是否存在config.json @@ -148,11 +97,11 @@ def check_require_config(): from auth import auth from main import main +from model import db check_require_config() -init_db() -#scheduler jobstore +# scheduler jobstore app.config['SCHEDULER_JOBSTORES'] = { 'default': SQLAlchemyJobStore(url='sqlite:///' + os.path.join(app.config['pandora_path'], DATABASE)) } @@ -160,14 +109,35 @@ def check_require_config(): scheduler.init_app(app) scheduler.start() +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(app.config['pandora_path'], DATABASE) +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True +db.init_app(app) + + +def include_object(object, name, type_, reflected, compare_to): + if ( + type_ == "table" and name == "apscheduler_jobs" + ): + return False + else: + return True + + +migrate = Migrate(include_object=include_object) +migrate.init_app(app, db) + + +def format_datetime(value): + """Format a datetime to a string.""" + if value is None: + return "" + return value.strftime('%Y-%m-%d %H:%M:%S') + def create_app(): app.register_blueprint(auth.auth_bp, url_prefix='/' + app.config['proxy_api_prefix']) app.register_blueprint(main.main_bp, url_prefix='/' + app.config['proxy_api_prefix']) - # 设置日志等级 - import logging - logging.basicConfig() - logging.getLogger('apscheduler').setLevel(logging.DEBUG) + app.jinja_env.filters['datetime'] = format_datetime return app diff --git a/login_tools.py b/login_tools.py index 87a3e5c..318fe16 100644 --- a/login_tools.py +++ b/login_tools.py @@ -19,9 +19,11 @@ def login(username, password): 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.request("POST", host + "/api/auth/login", headers=headers, data=payload) + logger.info("登录结果:{},{}", response.text, response.status_code) if response.status_code != 200: + if response.status_code == 404: + raise Exception("接口不存在,请检查Pandora是否配置正确") raise Exception(response.text) - logger.info("登录结果:{}", response.json()) return response.json() @@ -33,7 +35,42 @@ def get_access_token(session_token): 'Content-Type': 'application/x-www-form-urlencoded' } response = requests.request("POST", host + "/api/auth/session", headers=headers, data=payload) - logger.info("获取access_token结果:{},{}", response.json(), response.status_code) + logger.info("获取access_token结果:{},{}", response.text, response.status_code) + if response.status_code != 200: + raise Exception("获取access_token失败") + return response.json() + + +# 获取Refresh Token +def get_refresh_token(username, password): + host = get_host() + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + payload = { + 'username': username, + 'password': password + } + response = requests.request("POST", host + "/api/auth/login2", headers=headers, data=payload) + logger.info("获取refresh_token结果:{},{}", response.text, response.status_code) + + if response.status_code != 200: + raise Exception("获取refresh_token失败") + + return response.json() + + +# 使用refresh_token获取access_token /api/auth/refresh +def get_access_token_by_refresh_token(refresh_token): + host = get_host() + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + payload = { + 'refresh_token': refresh_token + } + response = requests.request("POST", host + "/api/auth/refresh", headers=headers, data=payload) + logger.info("获取access_token结果:{},{}", response.text, response.status_code) if response.status_code != 200: raise Exception("获取access_token失败") return response.json() diff --git a/main/main.py b/main/main.py index fee483e..a7d2e07 100644 --- a/main/main.py +++ b/main/main.py @@ -2,13 +2,14 @@ from datetime import datetime from loguru import logger -from flask import Blueprint, render_template, request, redirect, url_for, g, flash, jsonify +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required +from model import db import login_tools import share_tools import pandora_tools - +from model import User main_bp = Blueprint('main', __name__) @@ -16,13 +17,12 @@ @main_bp.route('/manage-users') @login_required def manage_users(): - from app import query_db,scheduler - users = query_db("select * from users") + from app import scheduler + users = db.session.query(User).all() # 将share_list转换为json对象 for user in users: - user['share_list'] = json.loads(user['share_list']) + user.share_list = json.loads(user.share_list) job_started = scheduler.get_job(id='my_job') is not None - return render_template('manage_users.html', users=users, job_started=job_started, balance=pandora_tools.get_balance()) @@ -32,49 +32,68 @@ def manage_users(): def add_user(): email = request.form['email'] password = request.form['password'] + custom_token_type = request.form['custom_token_type'] + custom_token = request.form['custom_token'] + if 'shared' in request.form: shared = 1 if request.form['shared'] == 'on' else 0 else: shared = 0 - g.db.execute('insert into users (email, password, shared, share_list, create_time, update_time) values (?, ?, ?, ' - '?, ?, ?)', - [email, password, shared, '[]', datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - datetime.now().strftime("%Y-%m-%d %H:%M:%S")]) - g.db.commit() + + user = User(email=email, password=password, shared=shared, + share_list='[]', + create_time=datetime.now(), + update_time=datetime.now()) + + if custom_token != '': + if custom_token_type == 'session_token': + user.session_token = custom_token + elif custom_token_type == 'refresh_token': + user.refresh_token = custom_token + + if custom_token == '' and custom_token_type == 'refresh_token': + try: + res = login_tools.get_refresh_token(email, password) + user.refresh_token = res['refresh_token'] + except Exception as e: + logger.error(e) + + db.session.add(user) + db.session.commit() + return redirect(url_for('main.manage_users')) @main_bp.route('/delete-user/') @login_required def delete_user(user_id): - g.db.execute('delete from users where id = ?', [user_id]) - g.db.commit() + db.session.query(User).filter_by(id=user_id).delete() + db.session.commit() return redirect(url_for('main.manage_users')) @main_bp.route('/add-share', methods=['POST']) @login_required def add_share(): - from app import query_db user_id = request.form.get('user_id') unique_name = request.form.get('unique_name') password = request.form.get('password') - user = query_db('select * from users where id = ?', one=True, args=(user_id,)) + user = db.session.query(User).filter_by(id=user_id).first() try: - share_token = share_tools.get_share_token(user['access_token'], unique_name) + share_token = share_tools.get_share_token(user.access_token, unique_name) except Exception as e: flash('添加失败: ' + str(e), 'error') return redirect(url_for('main.manage_users')) - share_list = json.loads(user['share_list']) + share_list = json.loads(user.share_list) # 检查是否已经存在 for share in share_list: if share['unique_name'] == unique_name: # 删除原有的share_token share_list.remove(share) share_list.append({'unique_name': unique_name, 'password': password, 'share_token': share_token['token_key']}) - g.db.execute( - f"UPDATE users SET share_list = '{json.dumps(share_list)}',update_time = '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}' WHERE id = {user_id}") - g.db.commit() + db.session.query(User).filter_by(id=user_id).update( + {'share_list': json.dumps(share_list), 'update_time': datetime.now()}) + db.session.commit() flash('添加成功', 'success') return redirect(url_for('main.manage_users')) @@ -82,17 +101,16 @@ def add_share(): @main_bp.route('/delete-share//') @login_required def delete_share(user_id, unique_name): - from app import query_db - user = query_db('select * from users where id = ?', one=True, args=(user_id,)) - share_list = json.loads(user['share_list']) + user = db.session.query(User).filter_by(id=user_id).first() + share_list = json.loads(user.share_list) for share in share_list: if share['unique_name'] == unique_name: share_list.remove(share) break - g.db.execute( - f"UPDATE users SET share_list = '{json.dumps(share_list)}',update_time = '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}' WHERE id = {user_id}") - g.db.commit() - share_tools.get_share_token(user['access_token'], unique_name, expires_in=-1) + db.session.query(User).filter_by(id=user_id).update( + {'share_list': json.dumps(share_list), 'update_time': datetime.now()}) + db.session.commit() + share_tools.get_share_token(user.access_token, unique_name, expires_in=-1) flash('删除成功', 'success') return redirect(url_for('main.manage_users')) @@ -100,15 +118,14 @@ def delete_share(user_id, unique_name): # 获取ShareToken用量信息 @main_bp.route('/share-info/') def share_info(user_id): - from app import query_db - user = query_db('select * from users where id = ?', one=True, args=(user_id,)) - if user['access_token'] is None: + user = db.session.query(User).filter_by(id=user_id).first() + if user.access_token is None: return jsonify({'code': 500, 'msg': '请先刷新'}) - share_list = json.loads(user['share_list']) + share_list = json.loads(user.share_list) dims = [] sources = [] for share in share_list: - info = share_tools.get_share_token_info(share['share_token'], user['access_token']) + info = share_tools.get_share_token_info(share['share_token'], user.access_token) if 'usage' in info: if 'range' in info['usage']: # 删除range键值对 @@ -133,35 +150,73 @@ def share_info(user_id): def refresh(user_id): - from app import query_db - user = query_db('select * from users where id = ?', one=True, args=(user_id,)) + user = db.session.query(User).filter_by(id=user_id).first() + if user is None: return redirect(url_for('main.manage_users')) - # 获取access_token - if user['session_token'] is None: - # 登录获取session_token 扣额度 + + def login_by_refresh_token(): try: - login_result = login_tools.login(user['email'], user['password']) + refresh_token_result = login_tools.get_access_token_by_refresh_token(user.refresh_token) + except Exception as e: + raise e + return refresh_token_result['access_token'] + + def login_by_password(): + try: + login_result = login_tools.login(user.email, user.password) except Exception as e: - logger.error(e) raise e access_token = login_result['access_token'] session_token = login_result['session_token'] - else: - # 使用session_token 刷新access_token + return access_token, session_token + + def login_by_session_token(): try: - access_token_result = login_tools.get_access_token(user['session_token']) + access_token_result = login_tools.get_access_token(user.session_token) except Exception as e: - logger.error(e) raise e access_token = access_token_result['access_token'] session_token = access_token_result['session_token'] - # 更新session_token 和 access_token - g.db.execute( - f"UPDATE users SET session_token = '{session_token}',access_token = '{access_token}',update_time = '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}' WHERE id = {user['id']}") - g.db.commit() + return access_token, session_token + + # 如果Refresh Token存在则使用Refresh Token刷新,否则使用Session Token刷新,都为空或失败则使用密码刷新保底 + # Refresh Token刷新仅更新access_token + # Session Token刷新和登录刷新,则更新access_token和session_token,并且以后使用Session Token刷新 + + access_token, session_token = None, None + + if user.refresh_token is not None: + try: + access_token = login_by_refresh_token() + db.session.query(User).filter_by(id=user_id).update( + {'access_token': access_token, 'update_time': datetime.now()}) + db.session.commit() + except Exception as e: + logger.error(e) + + else: + if user.session_token is not None: + try: + access_token, session_token = login_by_session_token() + db.session.query(User).filter_by(id=user_id).update( + {'access_token': access_token, 'session_token': session_token, 'update_time': datetime.now()}) + db.session.commit() + except Exception as e: + logger.error(e) + + if access_token is None: + try: + access_token, session_token = login_by_password() + db.session.query(User).filter_by(id=user_id).update( + {'access_token': access_token, 'session_token': session_token, 'update_time': datetime.now()}) + db.session.commit() + except Exception as e: + logger.error(e) + raise e + # 刷新share_token - share_list = json.loads(user['share_list']) + share_list = json.loads(user.share_list) if len(share_list) == 0: # 没有share token,直接返回 return redirect(url_for('main.manage_users')) @@ -174,30 +229,30 @@ def refresh(user_id): raise e share['share_token'] = share_token['token_key'] # 更新share_list - g.db.execute( - f"UPDATE users SET share_list = '{json.dumps(share_list)}',update_time = '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}' WHERE id = {user['id']}") - g.db.commit() + db.session.query(User).filter_by(id=user_id).update( + {'share_list': json.dumps(share_list), 'update_time': datetime.now()}) + db.session.commit() + return redirect(url_for('main.manage_users')) def refresh_all_user(): - from app import scheduler, app + from app import scheduler flag = False with scheduler.app.app_context(): - from app import query_db - users = query_db('select * from users') + users = db.session.query(User).all() for user in users: try: # jwt解析access_token 检查access_token是否过期 - if 'access_token' not in user or user['access_token'] is None: + if user.access_token is None: continue else: - token_info = pandora_tools.get_email_by_jwt(user['access_token']) + token_info = pandora_tools.get_email_by_jwt(user.access_token) # 根据exp判断是否过期,如果过期则刷新 exp_time = datetime.fromtimestamp(token_info['exp']) if exp_time > datetime.now(): continue flag = True - refresh(user['user_id']) + refresh(user.id) except Exception as e: logger.error(e) if flag: @@ -235,17 +290,16 @@ def refresh_route(user_id): def make_json(): - from app import query_db from flask import current_app import os - users = query_db("select * from users") + users = db.session.query(User).all() with open(os.path.join(current_app.config['pandora_path'], 'tokens.json'), 'r') as f: tokens = json.loads(f.read()) # 将share_list转换为json对象 for user in users: # 当存在share_list时, 取所有share_token, 并写入tokens.json - if user['share_list'] is not None and user['share_list'] != '[]': - share_list = json.loads(user['share_list']) + if user.share_list is not None and user.share_list != '[]': + share_list = json.loads(user.share_list) for share in share_list: # json中有share_token和password才写入 if 'share_token' in share and 'password' in share: @@ -254,26 +308,26 @@ def make_json(): 'password': share['password'] } else: - if user['session_token'] is None: + if user.session_token is None: continue # 当不存在share_list时, 取session_token, 并写入tokens.json # Todo 自定义 plus / show_user_info - tokens[user['email']] = { - 'token': user['access_token'], + tokens[user.email] = { + 'token': user.access_token, # Todo 自定义 'show_user_info': False, } - if user['shared'] == 1: - tokens[user['email']]['shared'] = True + if user.shared == 1: + tokens[user.email]['shared'] = True else: - tokens[user['email']]['shared'] = False - tokens[user['email']]['password'] = user['password'] + tokens[user.email]['shared'] = False + tokens[user.email]['password'] = user.password # 检测当前是否存在tokens.json,如果有则备份,文件名为tokens.json + 当前时间 - if os.path.exists(os.path.join(current_app.config['pandora_path'], 'tokens.json')): import time os.rename(os.path.join(current_app.config['pandora_path'], 'tokens.json'), - os.path.join(current_app.config['pandora_path'], 'tokens.json.' + time.strftime("%Y%m%d%H%M%S", time.localtime()))) + os.path.join(current_app.config['pandora_path'], + 'tokens.json.' + time.strftime("%Y%m%d%H%M%S", time.localtime()))) # 将数据写入tokens.json with open(os.path.join(current_app.config['pandora_path'], 'tokens.json'), 'w') as f: diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/1b720c8f6545_.py b/migrations/versions/1b720c8f6545_.py new file mode 100644 index 0000000..c4d3474 --- /dev/null +++ b/migrations/versions/1b720c8f6545_.py @@ -0,0 +1,55 @@ +"""empty message + +Revision ID: 1b720c8f6545 +Revises: +Create Date: 2023-12-30 15:27:33.599980 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + + +# revision identifiers, used by Alembic. +revision = '1b720c8f6545' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # 获取当前绑定的引擎 + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + # 检查users表是否存在 + if 'users' in inspector.get_table_names(): + # 获取users表的列名 + columns = [column['name'] for column in inspector.get_columns('users')] + # 如果refresh_token列不存在,则添加它 + if 'refresh_token' not in columns: + op.add_column('users', sa.Column('refresh_token', sa.Text(), nullable=True)) + else: + # 如果users表不存在,创建它,确保包括所有字段 + op.create_table('users', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('email', sa.Text(), nullable=True), + sa.Column('password', sa.Text(), nullable=True), + sa.Column('session_token', sa.Text(), nullable=True), + sa.Column('access_token', sa.Text(), nullable=True), + sa.Column('share_list', sa.Text(), nullable=True), + sa.Column('create_time', sa.DateTime(), nullable=True), + sa.Column('update_time', sa.DateTime(), nullable=True), + sa.Column('shared', sa.Integer(), nullable=True), + sa.Column('refresh_token', sa.Text(), nullable=True) # 新增的refresh_token字段 + ) + + +def downgrade(): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + # 如果users表存在,尝试移除refresh_token列 + if 'users' in inspector.get_table_names(): + if 'refresh_token' in [column['name'] for column in inspector.get_columns('users')]: + op.drop_column('users', 'refresh_token') \ No newline at end of file diff --git a/model.py b/model.py new file mode 100644 index 0000000..abbb9ed --- /dev/null +++ b/model.py @@ -0,0 +1,20 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +class User(db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.Text) + password = db.Column(db.Text) + session_token = db.Column(db.Text) + access_token = db.Column(db.Text) + share_list = db.Column(db.Text) + create_time = db.Column(db.DateTime) + update_time = db.Column(db.DateTime) + shared = db.Column(db.Integer) + refresh_token = db.Column(db.Text) + + def __repr__(self): + return '' % self.username diff --git a/requirements.txt b/requirements.txt index 8e5ef06..9eb5357 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/templates/manage_users.html b/templates/manage_users.html index 2660a7a..d8cf95a 100644 --- a/templates/manage_users.html +++ b/templates/manage_users.html @@ -1,368 +1,445 @@ {% extends 'layout.html' %} {% set page_title = 'PandoraNext Helper' %} {% block content %} -
-
- - {# 操作栏#} -
-
- - {% if not job_started %} - - - 定时刷新 +
+ - -