feat(project): 初始化项目结构和配置
- 添加.gitignore和.flaskenv环境配置文件 - 创建Flask应用基础架构,包括models、services、utils模块 - 配置数据库模型User和Setting,集成SQLAlchemy和Alembic迁移 - 添加前端Vue项目结构,包含Element Plus组件库 - 配置前后端API路由和蓝prints模块 - 实现书籍下载服务BookService功能模块 - 添加代码规范配置.editorconfig、.oxfmtrc.json、.oxlintrc.json - 配置VSCode推荐插件和前端构建工具链 - 实现文件服务FileService和相关业务逻辑
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
download
|
||||
.venv
|
||||
flask-ui/node_modules
|
||||
.idea
|
||||
@@ -0,0 +1,26 @@
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from .extensions import db, migrate
|
||||
from .config import DevelopmentConfig, ProductionConfig, TestingConfig
|
||||
|
||||
|
||||
def create_app(config_name="development"):
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
app.config.from_object(DevelopmentConfig)
|
||||
|
||||
match config_name:
|
||||
case 'production':
|
||||
app.config.from_object(ProductionConfig)
|
||||
case 'testing':
|
||||
app.config.from_object(TestingConfig)
|
||||
case _:
|
||||
app.config.from_object(DevelopmentConfig)
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
|
||||
from .blueprints.api import bp as api_bp
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,91 @@
|
||||
from typing import List
|
||||
from flask import Blueprint, jsonify, Response, current_app
|
||||
from app.services import FileService, VideoService, TorrentService, BookService, ImageService, MusicService
|
||||
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
|
||||
@bp.route("/book/download")
|
||||
def book_download():
|
||||
book_object = BookService()
|
||||
app = current_app._get_current_object()
|
||||
|
||||
def generate():
|
||||
with app.app_context():
|
||||
yield from book_object.book_download()
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream')
|
||||
|
||||
@bp.route("/video/findDeduplication")
|
||||
def video_find_deduplication():
|
||||
video_object = VideoService(paths=[
|
||||
"C:\\迅雷下载\\0_done",
|
||||
"D:\\",
|
||||
"E:\\",
|
||||
"F:\\"
|
||||
])
|
||||
|
||||
return jsonify({"code": 200, "message": video_object.find_deduplication()})
|
||||
|
||||
|
||||
@bp.route('/video/deduplication')
|
||||
def video_deduplication():
|
||||
video_object = VideoService(paths=[
|
||||
"C:\\迅雷下载\\0_done",
|
||||
"D:\\",
|
||||
"E:\\",
|
||||
"F:\\"
|
||||
])
|
||||
|
||||
return jsonify({"code": 200, "message": video_object.deduplication()})
|
||||
|
||||
|
||||
@bp.route("/video/hasTorrent")
|
||||
def video_has_torrent():
|
||||
torrent_obj = TorrentService(["E:\\"])
|
||||
return jsonify({"code": 200, "message": torrent_obj.has_torrent()})
|
||||
|
||||
|
||||
@bp.route('/files/rename')
|
||||
def files_rename():
|
||||
paths: List[str] = [
|
||||
"C:\\迅雷下载\\0_done",
|
||||
"C:\\Users\\Localhost\\Desktop\\BT",
|
||||
]
|
||||
|
||||
file_object = FileService(paths)
|
||||
return jsonify({"code": 200, "message": file_object.rename()})
|
||||
|
||||
|
||||
@bp.route('/torrent/deduplication')
|
||||
def torrent_deduplication():
|
||||
torrent_object = TorrentService(["C:\\Users\\Localhost\\Desktop\\BT"])
|
||||
return jsonify({"code": 200, "message": torrent_object.deduplication()})
|
||||
|
||||
|
||||
@bp.route("/image/export")
|
||||
def image_export():
|
||||
image_object = ImageService()
|
||||
|
||||
def generate():
|
||||
yield from image_object.export()
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream')
|
||||
|
||||
|
||||
@bp.route("/image/hasTorrent")
|
||||
def image_has_torrent():
|
||||
torrent_obj = TorrentService(["C:\\Users\\Localhost\\Desktop\\Images"])
|
||||
return jsonify({"code": 200, "message": torrent_obj.has_torrent()})
|
||||
|
||||
|
||||
@bp.route("/music/rename")
|
||||
def music_rename():
|
||||
music_object = MusicService()
|
||||
return jsonify({"code": 200, "message": music_object.rename()})
|
||||
|
||||
@bp.route("/torrent/statistics")
|
||||
def torrent_statistics():
|
||||
torrent_object = TorrentService(["C:\\Users\\Localhost\\Desktop\\BT"])
|
||||
return jsonify({"code": 200, "message": torrent_object.statistics()})
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
class Config:
|
||||
SECRET_KEY = 'mysecretkey'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///flask.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
DEBUG = False
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
||||
@@ -0,0 +1,17 @@
|
||||
import os
|
||||
IGNORE_DIRS = {
|
||||
"System Volume Information",
|
||||
"$RECYCLE.BIN",
|
||||
"Prevent Disk Sleep.txt",
|
||||
"PREVENT DISK SLEEP.txt",
|
||||
"images",
|
||||
"done_images",
|
||||
"女同"
|
||||
}
|
||||
SPECIAL_PREFIXES = {"T28", "FC2"}
|
||||
SEPARATORS = {"♀ ", "♀ ", "+"}
|
||||
SPECIAL_DIRS = {"川村まや", "上原亜衣", "上原志織"}
|
||||
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg'}
|
||||
DISLIKE_DIRS = {"篠真有", "二羽紗愛", "綾瀬天"}
|
||||
USERPROFILE = os.environ.get("USERPROFILE")
|
||||
TORRENT_ROOT_FOLDER: str = os.path.join(USERPROFILE, "Desktop\\BT")
|
||||
@@ -0,0 +1,5 @@
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
@@ -0,0 +1,2 @@
|
||||
from .user import User
|
||||
from .setting import Setting
|
||||
@@ -0,0 +1,29 @@
|
||||
from ..extensions import db
|
||||
|
||||
|
||||
class Setting(db.Model):
|
||||
name = db.Column(db.String(64), nullable=False)
|
||||
mod = db.Column(db.String(64), index=True, unique=True)
|
||||
fid = db.Column(db.Integer, primary_key=True)
|
||||
orderby = db.Column(db.String(64))
|
||||
baseUrl = db.Column(db.String(64))
|
||||
targetDate = db.Column(db.String(64))
|
||||
targetCountdownPage = db.Column(db.Integer)
|
||||
totalPage = db.Column(db.Integer)
|
||||
privilegeLevel = db.Column(db.Integer)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Setting %r>' % self.name
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'mod': self.mod,
|
||||
'fid': self.fid,
|
||||
'orderby': self.orderby,
|
||||
'baseUrl': self.baseUrl,
|
||||
'targetDate': self.targetDate,
|
||||
'targetCountdownPage': self.targetCountdownPage,
|
||||
'totalPage': self.totalPage,
|
||||
'privilegeLevel': self.privilegeLevel
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
from ..extensions import db
|
||||
|
||||
|
||||
class Torrent(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64), index=True, unique=True)
|
||||
captions_exists = db.Column(db.Boolean, server_default=db.false())
|
||||
captions_effective = db.Column(db.Boolean, server_default=db.false())
|
||||
|
||||
def __repr__(self):
|
||||
return '<Torrent %r>' % self.id
|
||||
@@ -0,0 +1,17 @@
|
||||
from ..extensions import db
|
||||
# 定义模型类(数据表)
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
# 将对象转换为字典,方便返回 JSON
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'username': self.username,
|
||||
'email': self.email
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
from .folder_service import FolderService
|
||||
from .file_service import FileService
|
||||
from .torrent_service import TorrentService
|
||||
from .book_service import BookService
|
||||
from .image_service import ImageService
|
||||
from .music_service import MusicService
|
||||
from .video_service import VideoService
|
||||
@@ -0,0 +1,163 @@
|
||||
import os
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from app.extensions import db
|
||||
from tenacity import retry, stop_after_attempt, RetryError
|
||||
from app.utils import datetime_to_timestamp, timestamp_to_datetime, get_today_timestamp
|
||||
from app.models import Setting
|
||||
|
||||
|
||||
class BookService:
|
||||
def __init__(self):
|
||||
self.setting = Setting.query.filter_by(name="book_download").first().to_dict()
|
||||
self.setting["targetDate"] = timestamp_to_datetime(datetime_to_timestamp(self.setting["targetDate"]) - 24 * 60 * 60)
|
||||
app_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
self.download_path = os.path.join(os.path.dirname(app_dir), "download") # 下载目录
|
||||
self.today_timestamp = get_today_timestamp()
|
||||
self.one_date_loading = False
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
"cookie": "cPNj_2132_saltkey=CeJLs8Ed; "
|
||||
"cPNj_2132_auth=44bdzFfx4TYjBMhuKf4AZdi2JM%2BXZ0Okxd0JEl1Hmwbz%2Fr3WiX0sLmATmolWkLlRgorFz%2BULizz5o6G%2FJ3YGWYPF0xw; "
|
||||
}
|
||||
|
||||
def legitimate_naming(self, name): # 合法命名
|
||||
name = name.replace(":", ":")
|
||||
name = name.replace("<", "《")
|
||||
name = name.replace(">", "》")
|
||||
name = name.replace("/", " ")
|
||||
name = name.replace("\\", " ")
|
||||
name = name.replace("?", "?")
|
||||
return name
|
||||
|
||||
def create_folder(self, target_dir, name): # 创建文件夹
|
||||
real_dir = os.path.join(target_dir, name)
|
||||
folder = os.path.exists(real_dir)
|
||||
if not folder:
|
||||
os.makedirs(real_dir)
|
||||
else:
|
||||
print(f"文件夹:{name} 已存在")
|
||||
|
||||
def isDownloadTarget(self, soup): # 是否是可以下载的文件
|
||||
return soup.headers.get('Content-Type') in ['"application/octet-stream"', "application/octet-stream"]
|
||||
|
||||
def post_page(self, name, url): # 帖子页面
|
||||
response = requests.get(f"{self.setting["baseUrl"]}{url}", headers=self.headers)
|
||||
print(f"详情页面状态码:{str(response.status_code)}")
|
||||
soup = BeautifulSoup(response.text, 'lxml')
|
||||
resource_boxs = soup.select('ignore_js_op')
|
||||
for i in resource_boxs:
|
||||
download_dir_path = os.path.join(self.download_path, self.setting["targetDate"])
|
||||
self.create_folder(download_dir_path, name)
|
||||
try:
|
||||
for string in i.select_one(".tip.tip_4").stripped_strings:
|
||||
if "阅读权限: " in repr(string):
|
||||
privilege_level = int(
|
||||
repr(string).strip("'").strip('阅读权限: '))
|
||||
if privilege_level > self.setting["privilegeLevel"]:
|
||||
print("下载失败,权限等级不够")
|
||||
return
|
||||
yield from self.download_file(f"{self.setting["baseUrl"]}{i.select_one(
|
||||
'a')['href']}", name, i.select_one('a').string)
|
||||
except RetryError as e:
|
||||
print('重试三次也不成功')
|
||||
|
||||
@retry(stop=stop_after_attempt(3))
|
||||
def download_file(self, file_url, dir_name, file_name): # 下载文件
|
||||
file_res = requests.get(file_url, headers=self.headers, stream=True, allow_redirects=False)
|
||||
if file_res.status_code in (301, 302, 307, 308): # 非200状态码,重新请求
|
||||
location = file_res.headers["location"]
|
||||
print(f'跳转页面了:{location}')
|
||||
file_res = requests.get(location, headers={"referer": location}, stream=True)
|
||||
print(f"下载状态码:{file_res.status_code} {file_url}")
|
||||
print(dir_name + "/" + file_name)
|
||||
yield f"data: {self.setting["targetDate"]} {file_name}\n\n"
|
||||
if self.isDownloadTarget(file_res):
|
||||
with open(f"download/{self.setting["targetDate"]}/{dir_name}/{file_name}", 'wb') as file2:
|
||||
for chunk in file_res.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
file2.write(chunk)
|
||||
|
||||
def section_page(self, page): # 版块页面
|
||||
print(f"当前页面 {page}")
|
||||
target_date_timestamp = datetime_to_timestamp(self.setting["targetDate"]) # 目标时间戳
|
||||
if target_date_timestamp >= self.today_timestamp: # 判断时间(是否大于当前时间戳)
|
||||
# print("不允许下载当天的")
|
||||
yield f"data: 不允许下载当天的\n\n"
|
||||
return None
|
||||
# yield f"data: 当前页面 {page}\n\n"
|
||||
url = f"{self.setting["baseUrl"]}forum.php?mod={self.setting["mod"]}&fid={self.setting["fid"]}&page={page}" # 版块页面地址
|
||||
response = requests.get(url, headers=self.headers) # 请求
|
||||
soup = BeautifulSoup(response.text, 'lxml') # 解析
|
||||
total_page_element = soup.select_one("a.bm_h") # 获取总页数元素
|
||||
if total_page_element is not None: # 总页数元素存在 则更新总页数
|
||||
new_total_page = int(total_page_element.get("totalpage")) # 获取总页数
|
||||
if self.setting["totalPage"] != new_total_page: # 总页数有更新
|
||||
self.setting["totalPage"] = new_total_page # 更新总页数
|
||||
Setting.query.filter_by(name="book_download").update({"totalPage": self.setting["totalPage"]})
|
||||
db.session.commit()
|
||||
yield from self.section_page(new_total_page - self.setting["targetCountdownPage"]) # 重新跳转指点版块页面
|
||||
return None
|
||||
|
||||
posts = soup.select("table#threadlisttableid tbody[id^=normalthread]") # 获取帖子列表
|
||||
posts.reverse() # 帖子列表倒序
|
||||
posts_min_timestamp = datetime_to_timestamp(self.get_post_date(posts[0])) # 帖子列表最小时间戳
|
||||
posts_max_timestamp = datetime_to_timestamp(self.get_post_date(posts[-1])) # 帖子列表最大时间戳
|
||||
|
||||
# 本页最早的帖子时间戳小于等于目标时间戳 且 倒计时页数大于 0 则跳转到上一页继续查找
|
||||
if posts_min_timestamp >= target_date_timestamp and not self.one_date_loading and self.setting["targetCountdownPage"] > 0:
|
||||
print("跳转到上一页")
|
||||
# yield f"data: 跳转到上一页\n\n"
|
||||
self.setting["targetCountdownPage"] = max(self.setting["targetCountdownPage"] - 1, 0)
|
||||
Setting.query.filter_by(name="book_download").update({"targetCountdownPage": self.setting["targetCountdownPage"]})
|
||||
db.session.commit()
|
||||
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
|
||||
return None
|
||||
else:
|
||||
print("不需要跳转上一页")
|
||||
self.one_date_loading = True
|
||||
# yield f"data: 不需要跳转上一页\n\n"
|
||||
for post in posts:
|
||||
post_element = post.select_one(".s.xst")
|
||||
post_date = self.get_post_date(post) # 获取帖子时间
|
||||
post_date_timestamp = datetime_to_timestamp(post_date) # 帖子时间戳
|
||||
if post_date_timestamp == target_date_timestamp: # 判断是否是目标时间
|
||||
print(f"目标时间是{post_date}")
|
||||
# yield f"data: 目标时间是{post_date}\n\n"
|
||||
yield from self.post_page(self.legitimate_naming(post_element.string), post_element['href']) # 跳转帖子页面
|
||||
if post_date_timestamp > target_date_timestamp: # 帖子时间大于目标时间并且没有下一天时间
|
||||
self.setting["targetDate"] = post_date
|
||||
Setting.query.filter_by(name="book_download").update({"targetDate": self.setting["targetDate"]})
|
||||
db.session.commit()
|
||||
print(f"下一天是 {post_date}")
|
||||
# yield f"data: 下一天是 {post_date}\n\n"
|
||||
self.one_date_loading = False
|
||||
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
|
||||
return None
|
||||
# 判断是否需要跳转到下一页
|
||||
if posts_max_timestamp <= target_date_timestamp and self.setting["targetCountdownPage"] < self.setting[
|
||||
"totalPage"] - 1:
|
||||
print("跳转到下一页")
|
||||
# yield f"data: 跳转到下一页\n\n"
|
||||
self.setting["targetCountdownPage"] = min(self.setting["targetCountdownPage"] + 1,
|
||||
self.setting["totalPage"] - 1)
|
||||
Setting.query.filter_by(name="book_download").update(
|
||||
{"targetCountdownPage": self.setting["targetCountdownPage"]})
|
||||
db.session.commit()
|
||||
yield from self.section_page(self.setting["totalPage"] - self.setting["targetCountdownPage"])
|
||||
return None
|
||||
|
||||
def get_post_date(self, element):
|
||||
if element.select_one(".by em span span"):
|
||||
if element.select_one(".by em span span").get("title"):
|
||||
return element.select_one(".by em span span").get("title")
|
||||
else:
|
||||
return element.select_one(".by em span span").string
|
||||
else:
|
||||
return element.select_one(".by em span").string
|
||||
|
||||
def book_download(self):
|
||||
self.create_folder(self.download_path, self.setting["targetDate"]) # 创建文件夹
|
||||
target_page = self.setting["totalPage"] - self.setting["targetCountdownPage"] # 目标页
|
||||
yield from self.section_page(target_page) # 前往目标页
|
||||
yield f"event: close\ndata: 已完成\n\n"
|
||||
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
from app.services import FolderService
|
||||
from app.utils import windows_sort_unicode
|
||||
from app.constants import IGNORE_DIRS, SEPARATORS, SPECIAL_PREFIXES, SPECIAL_DIRS
|
||||
|
||||
|
||||
class FileService:
|
||||
def __init__(self, paths: List[str]):
|
||||
self.paths = paths
|
||||
self.result_list: list[str] = []
|
||||
|
||||
def perform_rename(self, entry: os.DirEntry[str], new_name: str) -> Optional[str]:
|
||||
if new_name == entry.name:
|
||||
return None
|
||||
|
||||
new_path = os.path.join(os.path.dirname(entry.path), new_name)
|
||||
os.rename(entry.path, new_path)
|
||||
self.result_list.insert(0, new_path)
|
||||
# return new_path
|
||||
|
||||
def process_folder_name(self, entry: os.DirEntry[str]) -> Optional[str]:
|
||||
if not any(sep in entry.name for sep in SEPARATORS):
|
||||
return None
|
||||
|
||||
# 替换所有分隔符为+
|
||||
new_name = entry.name
|
||||
for sep in SEPARATORS:
|
||||
new_name = new_name.replace(sep, "+")
|
||||
|
||||
# 分割并排序各部分
|
||||
parts = new_name.split("+")
|
||||
sorted_parts = windows_sort_unicode(parts)
|
||||
new_name = "+".join(sorted_parts)
|
||||
|
||||
if new_name == entry.name:
|
||||
return None
|
||||
|
||||
return self.perform_rename(entry, new_name)
|
||||
|
||||
def process_file_name(self, entry: os.DirEntry[str]) -> Optional[str]:
|
||||
parent_dir = os.path.basename(os.path.dirname(entry.path))
|
||||
name, ext = os.path.splitext(entry.name)
|
||||
ext = ext.lower()
|
||||
name = name.upper()
|
||||
|
||||
# 排除特殊文件夹,仅处理大小写问题
|
||||
if parent_dir in SPECIAL_DIRS:
|
||||
return self.perform_rename(entry, f"{name}{ext}")
|
||||
|
||||
name = name.replace("-", "")
|
||||
|
||||
# 处理特殊前缀
|
||||
for prefix in SPECIAL_PREFIXES:
|
||||
if name.startswith(prefix):
|
||||
return self.perform_rename(entry, f"{prefix}-{name[len(prefix):]}{ext}")
|
||||
|
||||
# 在字母和数字之间添加连字符
|
||||
new_name = []
|
||||
for i, char in enumerate(name):
|
||||
if i > 0 and not name[i - 1].isnumeric() and char.isnumeric():
|
||||
new_name.append(f"-{char}")
|
||||
else:
|
||||
new_name.append(char)
|
||||
|
||||
if ''.join(new_name[-2:]) == "4K":
|
||||
new_name.insert(-2, "-")
|
||||
|
||||
return self.perform_rename(entry, f"{''.join(new_name)}{ext}")
|
||||
|
||||
def rename(self) -> List[str]:
|
||||
FolderService(paths=self.paths, ignore_dirs=IGNORE_DIRS, folder_callback=self.process_folder_name,
|
||||
file_callback=self.process_file_name).process_folder()
|
||||
return self.result_list
|
||||
@@ -0,0 +1,46 @@
|
||||
import os
|
||||
from typing import Optional, Callable, List
|
||||
|
||||
|
||||
class FolderService:
|
||||
def __init__(
|
||||
self,
|
||||
paths: List[str] | str,
|
||||
ignore_dirs: Optional[set] = None,
|
||||
folder_callback: Optional[Callable] = None,
|
||||
file_callback: Optional[Callable] = None,
|
||||
empty_folder_callback: Optional[Callable] = None,
|
||||
max_depth: Optional[int] = None
|
||||
):
|
||||
self.paths: List[str] = paths if type(paths) is not str else [paths]
|
||||
self.ignore_dirs = ignore_dirs or set()
|
||||
self.folder_callback = folder_callback
|
||||
self.file_callback = file_callback
|
||||
self.empty_folder_callback = empty_folder_callback
|
||||
self.max_depth = max_depth
|
||||
|
||||
def _traverse_folder(self, path: Optional[str] = None, depth: Optional[int] = 0) -> None:
|
||||
"""处理目录"""
|
||||
for entry in os.scandir(path):
|
||||
if entry.name in self.ignore_dirs:
|
||||
continue
|
||||
if entry.is_dir():
|
||||
# 判断要加载的文件夹深度
|
||||
if self.max_depth is not None and depth >= self.max_depth:
|
||||
return
|
||||
# 循环加载
|
||||
self._traverse_folder(entry.path, depth + 1)
|
||||
# 判断是空文件夹
|
||||
if not os.listdir(entry.path) and self.empty_folder_callback:
|
||||
self.empty_folder_callback(entry)
|
||||
# 文件夹处理的回调
|
||||
if self.folder_callback:
|
||||
self.folder_callback(entry)
|
||||
elif entry.is_file():
|
||||
if self.file_callback:
|
||||
self.file_callback(entry)
|
||||
|
||||
def process_folder(self) -> None:
|
||||
for path in self.paths:
|
||||
if os.path.exists(path):
|
||||
self._traverse_folder(path)
|
||||
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from app.services import FolderService
|
||||
from app.constants import IMAGE_EXTENSIONS, IGNORE_DIRS
|
||||
|
||||
|
||||
class ImageService:
|
||||
def __init__(self):
|
||||
self.prefix_pattern = re.compile(r'(?i)[A-Z]+\.')
|
||||
self.paths = ["C:\\Users\\Localhost\\Desktop\\images"]
|
||||
self.image_root_folder = "C:\\Users\\Localhost\\Desktop\\images"
|
||||
self.done_image = "C:\\Users\\Localhost\\Desktop\\BT\\done_images"
|
||||
|
||||
def export_callback(self, entry: os.DirEntry[str]) -> None:
|
||||
name, ext = os.path.splitext(entry.name)
|
||||
ext = ext.lower()
|
||||
if ext in IMAGE_EXTENSIONS:
|
||||
file_parent_dir = os.path.basename(os.path.dirname(entry.path))
|
||||
dest_dir = os.path.join(self.image_root_folder, file_parent_dir)
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
dest_file = os.path.join(dest_dir, entry.name)
|
||||
if os.path.exists(dest_file):
|
||||
yield f"data: {entry.name} 已存在\n\n"
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.move(entry.path, dest_file)
|
||||
except shutil.Error as e:
|
||||
yield f"data: 移动失败: {e}\n\n"
|
||||
|
||||
def export(self):
|
||||
FolderService(self.paths, ignore_dirs=IGNORE_DIRS, file_callback=self.export_callback).process_folder()
|
||||
|
||||
# 去结尾字母,已使用过了
|
||||
def remove_last_letter(self, entry: os.DirEntry[str]):
|
||||
without_modifier_file_name = self.prefix_pattern.sub('.', entry.name)
|
||||
os.rename(entry.path, os.path.join(os.path.dirname(entry.path), without_modifier_file_name))
|
||||
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
from typing import List
|
||||
from app.services import FolderService
|
||||
from app.utils import windows_sort_unicode
|
||||
|
||||
class MusicService:
|
||||
def __init__(self):
|
||||
self.paths: List[str] = ["C:\\Users\\Localhost\\Desktop\\Music"]
|
||||
self.result_list: List[str] = []
|
||||
|
||||
def perform_rename(self, entry: os.DirEntry[str]):
|
||||
name, ext = os.path.splitext(entry.name)
|
||||
music_artist, music_name = name.split(" - ")
|
||||
music_artist_list = music_artist.split("、")
|
||||
sorted_music_artist_list = windows_sort_unicode(music_artist_list)
|
||||
new_music_artist = "、".join(sorted_music_artist_list)
|
||||
if music_artist != new_music_artist:
|
||||
self.result_list.insert(0, f"{new_music_artist} - {music_name}")
|
||||
|
||||
def rename(self):
|
||||
FolderService(paths=self.paths, ignore_dirs={"old", "专辑"}, file_callback=self.perform_rename).process_folder()
|
||||
return self.result_list
|
||||
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
|
||||
from app.constants import IGNORE_DIRS
|
||||
from app.services import FolderService
|
||||
from app.utils import to_json_serializable
|
||||
|
||||
|
||||
class TorrentService:
|
||||
def __init__(self, paths: List[str]):
|
||||
self.paths: List[str] = paths
|
||||
self.torrent_root_folder = ["C:\\Users\\Localhost\\Desktop\\BT"]
|
||||
self.prefix_pattern = re.compile(r'(?i)[A-Z]+\.')
|
||||
self.torrent_list = defaultdict(set)
|
||||
self.list_of_duplicate_torrent: List[str] = []
|
||||
self.result_list = []
|
||||
self.torrent_name = defaultdict(lambda: defaultdict(set))
|
||||
|
||||
def process_torrent_file(self, entry: os.DirEntry[str]) -> None:
|
||||
temp_name = entry.name.replace("-4K", "")
|
||||
without_modifier_file_name = self.prefix_pattern.sub('.', temp_name)
|
||||
name, ext = os.path.splitext(without_modifier_file_name)
|
||||
if name not in self.torrent_list:
|
||||
self.torrent_list[name].add(os.path.basename(os.path.dirname(entry.path)))
|
||||
else:
|
||||
for torrent_path in self.torrent_list[name]:
|
||||
if not os.path.exists(os.path.join(os.path.dirname(entry.path), f"{name}A{ext}")):
|
||||
self.list_of_duplicate_torrent.insert(0, name)
|
||||
print(f"data: {entry.name} 已有种子: {torrent_path}")
|
||||
|
||||
def generate_torrent_list(self):
|
||||
folder_obj = FolderService(paths=self.paths, ignore_dirs=IGNORE_DIRS, file_callback=self.process_torrent_file)
|
||||
folder_obj.process_folder()
|
||||
return self.torrent_list
|
||||
|
||||
def deduplication(self):
|
||||
self.generate_torrent_list()
|
||||
return self.list_of_duplicate_torrent
|
||||
|
||||
def has_torrent_callback(self, entry: os.DirEntry):
|
||||
temp_name = entry.name
|
||||
temp_name = temp_name.replace("-4K", "")
|
||||
without_modifier_file_name = self.prefix_pattern.sub('.', temp_name)
|
||||
name, ext = os.path.splitext(without_modifier_file_name)
|
||||
if name not in self.torrent_list:
|
||||
self.result_list.insert(0, f"{os.path.basename(os.path.dirname(entry.path))} {entry.name} 该文件无种子")
|
||||
|
||||
def has_torrent(self):
|
||||
FolderService(paths=self.torrent_root_folder, ignore_dirs=IGNORE_DIRS,
|
||||
file_callback=self.process_torrent_file).process_folder()
|
||||
FolderService(paths=self.paths, ignore_dirs=IGNORE_DIRS,
|
||||
file_callback=self.has_torrent_callback).process_folder()
|
||||
return self.result_list
|
||||
|
||||
def statistics_file_callback(self, entry: os.DirEntry):
|
||||
path_arr = entry.path.split(os.sep)
|
||||
torrent_sub_type = {"0_多人", "FWAY", "0_女同", "_temp"}
|
||||
torrent_type = {"4k2", "4K原版", "456k", "1024", "2048", "done", "FC2", "hhd800", "other", "高清中文字幕",
|
||||
"三级写真", "无码流出", "亚洲有码原创"}
|
||||
if path_arr[-2] not in torrent_type and path_arr[-2] not in torrent_sub_type:
|
||||
if path_arr[-3] in torrent_sub_type:
|
||||
# print(path_arr[-3])
|
||||
if path_arr[-4] in torrent_type:
|
||||
# print(path_arr[-4])
|
||||
pass
|
||||
else:
|
||||
print(f"有奇怪的东西混入{path_arr}")
|
||||
pass
|
||||
elif path_arr[-3] in torrent_type:
|
||||
self.torrent_name[path_arr[-2]][path_arr[-3]].add(entry.name)
|
||||
pass
|
||||
else:
|
||||
print(f"有奇怪的东西混入2{path_arr}")
|
||||
|
||||
def statistics(self):
|
||||
FolderService(paths=self.paths,
|
||||
ignore_dirs=IGNORE_DIRS,
|
||||
file_callback=self.statistics_file_callback).process_folder()
|
||||
return to_json_serializable(self.torrent_name)
|
||||
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
|
||||
from app.constants import IGNORE_DIRS, TORRENT_ROOT_FOLDER
|
||||
from app.services import FolderService
|
||||
|
||||
|
||||
class VideoService:
|
||||
def __init__(self, paths: list[str]):
|
||||
self.paths: list[str] = paths
|
||||
self.prefix_pattern = re.compile(r'(?i)[A-Z]+\.')
|
||||
self.torrent_root_folder = "C:\\Users\\Localhost\\Desktop\\BT"
|
||||
self.torrent_folder_depth = len(TORRENT_ROOT_FOLDER.split(os.sep))
|
||||
self.new_paths: dict[str, dict[str, list[str]]] = defaultdict(dict)
|
||||
self.new_paths_2: dict[str, list[str]] = defaultdict(dict)
|
||||
self.current_folder = ""
|
||||
self.result_list: list[str] = []
|
||||
self.func_type = None
|
||||
|
||||
def process_torrent_file(self, entry: os.DirEntry):
|
||||
name, ext = os.path.splitext(self.prefix_pattern.sub('.', entry.name))
|
||||
if name not in self.new_paths[self.current_folder]:
|
||||
self.new_paths[self.current_folder][name] = []
|
||||
self.new_paths[self.current_folder][name].append(entry.path)
|
||||
if name not in self.new_paths_2:
|
||||
self.new_paths_2[name] = []
|
||||
self.new_paths_2[name].append(entry.path)
|
||||
# if self.func_type != "remove_deduplication":
|
||||
# if ext != ".mp4":
|
||||
# self.result_list.insert(0, f"重复文件:{name} {ext}")
|
||||
# if len(self.new_paths[self.current_folder][name]) > 1:
|
||||
# print(name, ext)
|
||||
|
||||
def get_init_folder(self, entry: os.DirEntry):
|
||||
temp_path: list[str] = []
|
||||
for path in self.paths:
|
||||
if os.path.exists(path) and os.path.exists(os.path.join(path, entry.name)):
|
||||
temp_path.append(os.path.join(path, entry.name))
|
||||
self.current_folder = entry.name
|
||||
FolderService(paths=temp_path, ignore_dirs=IGNORE_DIRS,
|
||||
file_callback=self.process_torrent_file,
|
||||
empty_folder_callback=self.remove_folder).process_folder()
|
||||
if self.func_type != "remove_deduplication":
|
||||
return
|
||||
FolderService(paths=entry.path, ignore_dirs=IGNORE_DIRS,
|
||||
file_callback=self.torrent_deduplication,
|
||||
empty_folder_callback=self.remove_folder).process_folder()
|
||||
|
||||
def torrent_deduplication(self, entry: os.DirEntry):
|
||||
without_modifier_file_name = self.prefix_pattern.sub('.', entry.name)
|
||||
name, ext = os.path.splitext(without_modifier_file_name)
|
||||
if name in self.new_paths[entry.path.split(os.sep)[self.torrent_folder_depth]]:
|
||||
folder_arr = entry.path.split(os.sep)[self.torrent_folder_depth:-1]
|
||||
folder_arr.insert(0, "done")
|
||||
new_path = os.path.join(TORRENT_ROOT_FOLDER, *folder_arr)
|
||||
print(os.path.join(new_path, entry.name))
|
||||
os.makedirs(new_path, exist_ok=True)
|
||||
try:
|
||||
shutil.move(entry.path, os.path.join(new_path, entry.name))
|
||||
self.result_list.insert(0, f"移动到文件夹:{os.path.join(new_path, entry.name)}")
|
||||
except shutil.Error:
|
||||
pass
|
||||
|
||||
def remove_folder(self, entry: os.DirEntry):
|
||||
if entry.name == self.current_folder:
|
||||
return
|
||||
os.rmdir(entry.path)
|
||||
self.result_list.insert(0, f"删除文件夹:{entry.path}")
|
||||
|
||||
def deduplication(self):
|
||||
self.func_type = "remove_deduplication"
|
||||
FolderService(paths=TORRENT_ROOT_FOLDER, ignore_dirs=IGNORE_DIRS | {"done"}, max_depth=1,
|
||||
folder_callback=self.get_init_folder).process_folder()
|
||||
return self.result_list
|
||||
|
||||
def find_deduplication(self):
|
||||
self.func_type = "find_deduplication"
|
||||
FolderService(paths=TORRENT_ROOT_FOLDER, ignore_dirs=IGNORE_DIRS | {"done"}, max_depth=1,
|
||||
folder_callback=self.get_init_folder).process_folder()
|
||||
for name in self.new_paths_2:
|
||||
if len(self.new_paths_2[name]) > 1:
|
||||
temp_name = []
|
||||
for path_2 in self.new_paths_2[name]:
|
||||
name_2 = os.path.splitext(os.path.basename(path_2))[0]
|
||||
if name_2 not in temp_name:
|
||||
temp_name.append(name_2)
|
||||
else:
|
||||
self.result_list.insert(0, name_2)
|
||||
print(name_2)
|
||||
if len(temp_name)>1 and name in temp_name:
|
||||
self.result_list.insert(0, name)
|
||||
return self.result_list
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mod": "forumdisplay",
|
||||
"fid": "139",
|
||||
"orderby": "dateline",
|
||||
"baseUrl": "http://127.0.0.1:20000/",
|
||||
"targetDate": "2026-03-23",
|
||||
"targetCountdownPage": 998,
|
||||
"totalPage": 999,
|
||||
"privilegeLevel": 20,
|
||||
"headers": {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
"cookie": "cPNj_2132_lastfp=079737cd852f396e59ea74a50e4ff9f1; cPNj_2132_saltkey=CeJLs8Ed; cPNj_2132_lastvisit=1726724743; _safe=h4uHX8t74U1uC89h; cPNj_2132_nofavfid=1; cPNj_2132_smile=1D1; cPNj_2132_secqaaqSARdM0=1286544.1fe30bcf776352790f; cPNj_2132_atarget=1; cPNj_2132__refer=%252Fhome.php%253Fmod%253Dspacecp%2526ac%253Dprofile%2526op%253Dpassword; cPNj_2132_auth=44bdzFfx4TYjBMhuKf4AZdi2JM%2BXZ0Okxd0JEl1Hmwbz%2Fr3WiX0sLmATmolWkLlRgorFz%2BULizz5o6G%2FJ3YGWYPF0xw; cPNj_2132_lastcheckfeed=508551%7C1726728824; cPNj_2132_lip=8.219.243.67%2C1726728824; cPNj_2132_secqaaqSAp9u0=1287401.84f80cf53671988988; cPNj_2132_home_diymode=1; cPNj_2132_sid=0; cPNj_2132_st_t=508551%7C1729318359%7C645e73a57f2807c1a85c994685fbe7b7; cPNj_2132_forum_lastvisit=D_151_1726729986D_36_1726732739D_95_1726751606D_103_1729304183D_139_1729318359; cPNj_2132_visitedfid=139D103D159D95D141D143D166D36D170D151; cPNj_2132_ulastactivity=1729318359%7C0; cf_clearance=wXzU4znYwA5CfAohAd3fPolFurbPcgWGacwTjhmMmQs-1729318361-1.2.1.1-YhnHHOA0XWfeDsF2TdvRrtWHzFTfFlZCf5l3u5AFdto2u16rpdDDZGgi6OkrKmJ6gw4bFgh1P1PcvBtfxw4wA3ME2BcVkNhYscsgDJ_V0Dgh91hgopC85KB4LlV2rcz_K0UL7NMyAgbhID4AC.eeLsjlPmxd_Xq8SJCDctTvfRimKb22bGEuyXA_bKL_k2X5lDLDJSXfjx3285g0X2WUQroJKymoOBHn0MBUYrnP1h_dyD7KzZYkj38J3_aiM22H_rtzKO3O2sD_fitnDRuM.BAJhdr2bYdrM_UXdJAj4IF3MY_BqlCyYHSX.AaVqOz3MyK7j7Dj_jv9PdThWwcA_kjRuK4hVmxoofcu._2tw5_EY28D2.0CDIgAbjidrIUW; cPNj_2132_st_p=508551%7C1729318366%7Cf7f4e3654f2de21700cdd0472289b3e2; cPNj_2132_viewid=tid_2380153; cPNj_2132_lastact=1729318373%09forum.php%09attachment"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
import time
|
||||
import ctypes
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from functools import cmp_to_key
|
||||
|
||||
|
||||
|
||||
def windows_sort_unicode(items: List[str]) -> List[str]:
|
||||
"""
|
||||
使用Windows API实现字符串列表的排序。
|
||||
|
||||
该函数通过调用Windows的Shlwapi.dll库中的StrCmpLogicalW函数进行字符串的比较,以实现自然排序。
|
||||
自然排序是指在排序数字和字母组合的字符串时,数字按照数值来比较,而不是按照字符的ASCII值来比较。
|
||||
|
||||
:param items: List[str] - 需要排序的字符串列表。
|
||||
:return: List[str] - 排序后的字符串列表。
|
||||
"""
|
||||
shlwapi = ctypes.windll.LoadLibrary('Shlwapi.dll')
|
||||
StrCmpLogicalW = shlwapi.StrCmpLogicalW
|
||||
StrCmpLogicalW.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p]
|
||||
StrCmpLogicalW.restype = ctypes.c_int
|
||||
|
||||
def compare(a: str, b: str) -> int:
|
||||
"""
|
||||
比较两个字符串的函数,用于在排序时调用。
|
||||
|
||||
:param a: str - 第一个字符串。
|
||||
:param b: str - 第二个字符串。
|
||||
:return: int - StrCmpLogicalW函数的返回值,表示两个字符串的比较结果。
|
||||
"""
|
||||
return StrCmpLogicalW(str(a), str(b))
|
||||
|
||||
return sorted(items, key=cmp_to_key(compare))
|
||||
|
||||
|
||||
def read_json_file(json_path): # 读取json文件
|
||||
with open(json_path, 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
return data
|
||||
|
||||
|
||||
def write_json_file(json_path, data): # 写入json文件
|
||||
with open(json_path, 'w') as file:
|
||||
json.dump(data, file, sort_keys=False, indent=4)
|
||||
|
||||
|
||||
def datetime_to_timestamp(date) -> int:
|
||||
"""
|
||||
日期转时间戳
|
||||
:param date: 需要转换的日期
|
||||
:return:
|
||||
"""
|
||||
return int(datetime.strptime(date, "%Y-%m-%d").timestamp())
|
||||
|
||||
|
||||
def timestamp_to_datetime(timestamp):
|
||||
"""
|
||||
时间戳转日期
|
||||
:param timestamp: 需要转换的时间戳
|
||||
:return:
|
||||
"""
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def get_today_timestamp():
|
||||
"""
|
||||
获取今日时间戳
|
||||
:return:
|
||||
"""
|
||||
return datetime_to_timestamp(datetime.now().strftime("%Y-%m-%d"))
|
||||
|
||||
|
||||
def show_delay_progress(delay_time: float) -> None:
|
||||
"""在控制台显示延迟进度条"""
|
||||
total_blocks = 30 # 进度条总长度
|
||||
start_time = time.time()
|
||||
end_time = start_time + delay_time
|
||||
|
||||
print(f"\n等待 {delay_time:.1f} 秒... [{' ' * total_blocks}]", end='', flush=True)
|
||||
|
||||
while time.time() < end_time:
|
||||
elapsed = time.time() - start_time
|
||||
progress = min(1.0, elapsed / delay_time)
|
||||
filled = int(progress * total_blocks)
|
||||
remaining = end_time - time.time()
|
||||
|
||||
print(f"\r等待 {delay_time:.1f} 秒... [{'=' * filled}>{' ' * (total_blocks - filled - 1)}]"
|
||||
f" 剩余: {remaining:.1f}s", end='', flush=True)
|
||||
time.sleep(0.1)
|
||||
|
||||
print("\r" + " " * 80 + "\r", end='', flush=True) # 清空进度行
|
||||
|
||||
def to_json_serializable(obj):
|
||||
"""递归将 defaultdict 和 set 转换为 JSON 可序列化的类型"""
|
||||
if isinstance(obj, defaultdict):
|
||||
# 转为普通字典,并递归处理每个值
|
||||
obj = dict(obj)
|
||||
if isinstance(obj, dict):
|
||||
return {k: to_json_serializable(v) for k, v in obj.items()}
|
||||
if isinstance(obj, set):
|
||||
return list(obj) # 集合转列表
|
||||
# 其他基本类型(str, int, float, list, tuple, None)直接返回
|
||||
return obj
|
||||
@@ -0,0 +1,97 @@
|
||||
# 初始化迁移仓库(只需执行一次)
|
||||
|
||||
```shell
|
||||
flask db init
|
||||
```
|
||||
|
||||
# 自定义模板
|
||||
|
||||
```shell
|
||||
flask db init --template mytemplate
|
||||
```
|
||||
|
||||
# 根据模型生成迁移脚本
|
||||
|
||||
```shell
|
||||
flask db migrate -m "创建 User 表"
|
||||
```
|
||||
|
||||
# 执行迁移,创建数据库表
|
||||
|
||||
```shell
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
# 升级到指定版本
|
||||
|
||||
```shell
|
||||
flask db upgrade +2 # 向前2个版本
|
||||
flask db upgrade ae10 # 升级到特定版本号
|
||||
```
|
||||
|
||||
# 降级
|
||||
|
||||
```shell
|
||||
flask db downgrade -1 # 回退1个版本
|
||||
flask db downgrade base # 回退到初始状态
|
||||
````
|
||||
|
||||
# 空迁移(手动编写SQL)
|
||||
|
||||
```shell
|
||||
flask db revision -m "手动修改"
|
||||
```
|
||||
|
||||
# 离线生成 SQL
|
||||
|
||||
```shell
|
||||
flask db upgrade --sql > migration.sql
|
||||
```
|
||||
|
||||
# 合并多个迁移文件
|
||||
|
||||
```shell
|
||||
flask db merge -m "合并分支" revision1 revision2
|
||||
```
|
||||
|
||||
# 标记数据库为已升级到指定版本(不执行迁移)
|
||||
|
||||
```shell
|
||||
flask db stamp revision_hash
|
||||
```
|
||||
|
||||
# 查看迁移历史
|
||||
|
||||
```shell
|
||||
flask db history
|
||||
```
|
||||
|
||||
# 查看当前版本
|
||||
|
||||
```shell
|
||||
flask db current
|
||||
```
|
||||
|
||||
# 显示可用命令
|
||||
|
||||
```shell
|
||||
flask db --help
|
||||
```
|
||||
|
||||
# 查看当前分支/head
|
||||
|
||||
```shell
|
||||
flask db heads
|
||||
```
|
||||
|
||||
# 查看所有分支点
|
||||
|
||||
```shell
|
||||
flask db branches
|
||||
```
|
||||
|
||||
# 显示特定版本
|
||||
|
||||
```shell
|
||||
flask db show <revision>
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,39 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
|
||||
# Vite
|
||||
*.timestamp-*-*.mjs
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"oxc.oxc-vscode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# flask-ui
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import { GlobalComponents } from 'vue'
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
||||
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
|
||||
// For TSX support
|
||||
declare global {
|
||||
const ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
const ElAside: typeof import('element-plus/es')['ElAside']
|
||||
const ElButton: typeof import('element-plus/es')['ElButton']
|
||||
const ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
const ElCard: typeof import('element-plus/es')['ElCard']
|
||||
const ElCol: typeof import('element-plus/es')['ElCol']
|
||||
const ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
const ElForm: typeof import('element-plus/es')['ElForm']
|
||||
const ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
const ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
const ElInput: typeof import('element-plus/es')['ElInput']
|
||||
const ElMain: typeof import('element-plus/es')['ElMain']
|
||||
const ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
const ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
||||
const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
const ElRow: typeof import('element-plus/es')['ElRow']
|
||||
const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
const ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
const ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
||||
const ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
||||
const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,26 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||
import skipFormatting from 'eslint-config-prettier/flat'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
...pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||
|
||||
skipFormatting,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+7291
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "flask-ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "run-s lint:*",
|
||||
"lint:oxlint": "oxlint . --fix",
|
||||
"lint:eslint": "eslint . --fix --cache",
|
||||
"format": "oxfmt src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.13.6",
|
||||
"element-plus": "^2.13.6",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "beta",
|
||||
"vue-router": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.11.0",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-oxlint": "~1.50.0",
|
||||
"eslint-plugin-vue": "~10.8.0",
|
||||
"jiti": "^2.6.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxfmt": "^0.35.0",
|
||||
"oxlint": "~1.50.0",
|
||||
"sass-embedded": "^1.98.0",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "beta",
|
||||
"vite-plugin-vue-devtools": "^8.0.6",
|
||||
"vue-tsc": "^3.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"overrides": {
|
||||
"vue": "beta",
|
||||
"@vue/compiler-core": "beta",
|
||||
"@vue/compiler-dom": "beta",
|
||||
"@vue/compiler-sfc": "beta",
|
||||
"@vue/compiler-ssr": "beta",
|
||||
"@vue/compiler-vapor": "beta",
|
||||
"@vue/reactivity": "beta",
|
||||
"@vue/runtime-core": "beta",
|
||||
"@vue/runtime-dom": "beta",
|
||||
"@vue/runtime-vapor": "beta",
|
||||
"@vue/server-renderer": "beta",
|
||||
"@vue/shared": "beta",
|
||||
"@vue/compat": "beta"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {RouterView} from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,67 @@
|
||||
import request from "../utils/request.ts"
|
||||
|
||||
export function book_download() {
|
||||
return request({
|
||||
url: "/book/download",
|
||||
})
|
||||
}
|
||||
|
||||
export function video_findDeduplication() {
|
||||
return request({
|
||||
url: "/video/findDeduplication",
|
||||
})
|
||||
}
|
||||
|
||||
export function video_deduplication() {
|
||||
return request({
|
||||
url: "/video/deduplication",
|
||||
})
|
||||
}
|
||||
|
||||
export function video_hasTorrent() {
|
||||
return request({
|
||||
url: "/video/hasTorrent",
|
||||
})
|
||||
}
|
||||
|
||||
export function files_rename() {
|
||||
return request({
|
||||
url: "/files/rename",
|
||||
})
|
||||
}
|
||||
|
||||
export function torrent_deduplication() {
|
||||
return request({
|
||||
url: "/torrent/deduplication",
|
||||
})
|
||||
}
|
||||
|
||||
export function image_export() {
|
||||
return request({
|
||||
url: "/image/export",
|
||||
})
|
||||
}
|
||||
|
||||
export function image_hasTorrent() {
|
||||
return request({
|
||||
url: "/image/hasTorrent",
|
||||
})
|
||||
}
|
||||
|
||||
export function music_rename() {
|
||||
return request({
|
||||
url: "/music/rename",
|
||||
})
|
||||
}
|
||||
|
||||
export function test() {
|
||||
return request({
|
||||
url: "/test",
|
||||
})
|
||||
}
|
||||
|
||||
export function torrent_statistics(){
|
||||
return request({
|
||||
url: "/torrent/statistics",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*@import './base.css';*/
|
||||
|
||||
/*#app {*/
|
||||
/* max-width: 1280px;*/
|
||||
/* margin: 0 auto;*/
|
||||
/* padding: 2rem;*/
|
||||
/* font-weight: normal;*/
|
||||
/*}*/
|
||||
|
||||
/*a,*/
|
||||
/*.green {*/
|
||||
/* text-decoration: none;*/
|
||||
/* color: hsla(160, 100%, 37%, 1);*/
|
||||
/* transition: 0.4s;*/
|
||||
/* padding: 3px;*/
|
||||
/*}*/
|
||||
|
||||
/*@media (hover: hover) {*/
|
||||
/* a:hover {*/
|
||||
/* background-color: hsla(160, 100%, 37%, 0.2);*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/*@media (min-width: 1024px) {*/
|
||||
/* body {*/
|
||||
/* display: flex;*/
|
||||
/* place-items: center;*/
|
||||
/* }*/
|
||||
|
||||
/* #app {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-columns: 1fr 1fr;*/
|
||||
/* padding: 0 2rem;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
|
||||
defineProps<{
|
||||
isCollapse: boolean
|
||||
}>()
|
||||
|
||||
const iconComponent = ref("location")
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-aside>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
default-active="2"
|
||||
:collapse="isCollapse"
|
||||
class="el-menu-vertical-demo"
|
||||
router>
|
||||
<el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="iconComponent"></component>
|
||||
</el-icon>
|
||||
<span>Navigator One</span>
|
||||
</template>
|
||||
<el-menu-item-group title="Group One">
|
||||
<el-menu-item index="1-1">
|
||||
<el-icon>
|
||||
<location/>
|
||||
</el-icon>
|
||||
item one
|
||||
</el-menu-item>
|
||||
<el-menu-item index="1-2">item two</el-menu-item>
|
||||
</el-menu-item-group>
|
||||
<el-menu-item-group title="Group Two">
|
||||
<el-menu-item index="1-3">item three</el-menu-item>
|
||||
</el-menu-item-group>
|
||||
<el-sub-menu index="1-4">
|
||||
<template #title>item four</template>
|
||||
<el-menu-item index="1-4-1">item one</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="2" route="about">
|
||||
<el-icon>
|
||||
<Menu/>
|
||||
</el-icon>
|
||||
<span>统计</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="3" disabled>
|
||||
<el-icon>
|
||||
<document/>
|
||||
</el-icon>
|
||||
<span>Navigator Three</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="4">
|
||||
<el-icon>
|
||||
<setting/>
|
||||
</el-icon>
|
||||
<span>Navigator Four</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-aside {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
max-height: calc(100vh - 60px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import AppMain from "@/layout/components/AppMain.vue"
|
||||
import Sidebar from "@/layout/components/Sidebar/index.vue"
|
||||
|
||||
let isCollapse = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<template #default>
|
||||
<el-radio-group v-model="isCollapse">
|
||||
<el-radio-button :value="false">expand</el-radio-button>
|
||||
<el-radio-button :value="true">collapse</el-radio-button>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<Sidebar :isCollapse="isCollapse"></Sidebar>
|
||||
<AppMain></AppMain>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-header) {
|
||||
border-bottom: 1px solid var(--el-menu-border-color);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.el-menu-vertical-demo {
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,35 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import Layout from '../layout/index.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '',
|
||||
redirect: 'index',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'Index',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
name: 'About',
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import axios, {
|
||||
type AxiosError,
|
||||
type AxiosInstance,
|
||||
type AxiosResponse,
|
||||
type InternalAxiosRequestConfig
|
||||
} from "axios"
|
||||
|
||||
// 定义响应数据结构
|
||||
interface ResponseData<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
interface RequestCache {
|
||||
url: string
|
||||
data: string
|
||||
time: number
|
||||
}
|
||||
|
||||
let downloadLoadingInstance: any
|
||||
export let isRelogin: { show: boolean } = {show: false}
|
||||
axios.defaults.headers["Content-Type"] = "application/json; charset=UTF-8"
|
||||
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: "http://localhost:5000/api/",
|
||||
// baseURL: "/api",
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const isToken = (config.headers || {}).isToken === false
|
||||
console.log(config)
|
||||
return config
|
||||
}, (error: AxiosError) => {
|
||||
// 请求错误处理
|
||||
console.error('请求拦截器错误:', error);
|
||||
return Promise.reject(error);
|
||||
})
|
||||
service.interceptors.response.use((res: AxiosResponse) => {
|
||||
const code = (res.data as ResponseData).code || 200
|
||||
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||
return res.data
|
||||
}
|
||||
if (code !== 200) {
|
||||
return Promise.reject("error")
|
||||
} else {
|
||||
return res.data
|
||||
}
|
||||
}, (error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
export default service
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {torrent_statistics} from '@/api'
|
||||
|
||||
interface StatisticsData {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let aa = ref<StatisticsData>({})
|
||||
let bb = ref("")
|
||||
let cc = ref("")
|
||||
let dd = ref("")
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await torrent_statistics()
|
||||
console.log(res.message)
|
||||
aa.value = res.message
|
||||
})
|
||||
|
||||
const getSecondLevelOptions = () => {
|
||||
if (!bb.value || !aa.value[bb.value]) {
|
||||
return []
|
||||
}
|
||||
return aa.value[bb.value]
|
||||
}
|
||||
|
||||
const getThirdLevelOptions = () => {
|
||||
if (!bb.value || !cc.value || !aa.value[bb.value] || !aa.value[bb.value][cc.value]) {
|
||||
return []
|
||||
}
|
||||
console.log(aa.value[bb.value])
|
||||
return aa.value[bb.value][cc.value]
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-main>
|
||||
<el-card shadow="never">
|
||||
<template #default>
|
||||
<div v-for="(v,i) in getSecondLevelOptions()">
|
||||
<div>{{i}}</div>
|
||||
<el-button v-for="(v2,i2) in v" :key="i2">{{ v2 }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-select style="width: 120px" placeholder="请选择" v-model="bb">
|
||||
<el-option v-for="(v,i) in aa" :key="i" :label="i" :value="i"></el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select style="width: 120px" placeholder="请选择" v-model="cc">
|
||||
<el-option v-for="(v,i) in getSecondLevelOptions()" :key="i" :label="i"
|
||||
:value="i"></el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select style="width: 120px" placeholder="请选择" v-model="dd">
|
||||
<el-option v-for="(v,i) in getThirdLevelOptions()" :key="i" :label="v"
|
||||
:value="v"></el-option>
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" @click="">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
:deep(.el-card__body) {
|
||||
//height: calc(100vh - 211px + 60px);
|
||||
height: calc(100vh - 211px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onUnmounted} from "vue";
|
||||
import {
|
||||
files_rename,
|
||||
music_rename,
|
||||
image_hasTorrent,
|
||||
video_hasTorrent,
|
||||
video_deduplication,
|
||||
torrent_deduplication,
|
||||
video_findDeduplication,
|
||||
torrent_statistics
|
||||
} from "@/api"
|
||||
|
||||
// 警告项接口
|
||||
interface messageItem {
|
||||
id: number
|
||||
text: string
|
||||
}
|
||||
|
||||
interface BtnItem {
|
||||
id: number
|
||||
title: string
|
||||
handle?: () => Promise<any>
|
||||
}
|
||||
|
||||
let loading = ref(false)
|
||||
let eventSource: EventSource // 保存 EventSource 实例
|
||||
const status = ref('closed') // 连接状态:closed, connecting, connected
|
||||
const messages = ref<messageItem[]>([{id: Date.now(), text: '输出结果'}])
|
||||
|
||||
const buttons = [
|
||||
{id: 0, title: "小说下载"},
|
||||
{id: 1, title: "文件重命名", handle: files_rename},
|
||||
{id: 2, title: "视频查找重复", handle: video_findDeduplication},
|
||||
{id: 3, title: "视频去重", handle: video_deduplication},
|
||||
{id: 4, title: "种子去重", handle: torrent_deduplication},
|
||||
{id: 5, title: "判断视频是否有torrent", handle: video_hasTorrent},
|
||||
{id: 6, title: "判断图片是否有torrent", handle: image_hasTorrent},
|
||||
{id: 7, title: "音乐文件重命名", handle: music_rename},
|
||||
{id: 8, title: "种子统计", handle: torrent_statistics}
|
||||
]
|
||||
|
||||
function closeAlert(id: number) {
|
||||
console.log(id)
|
||||
messages.value = messages.value.filter(item => item.id !== id)
|
||||
}
|
||||
|
||||
const handleClick = async (btnObj: BtnItem) => {
|
||||
if (btnObj.title === "断开") return
|
||||
if (btnObj.title === "小说下载") {
|
||||
connect("http://localhost:5000/api/book/download")
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await btnObj.handle?.()
|
||||
const res_msg = Array.isArray(res.message) ? res.message : [res.message]
|
||||
messages.value = res_msg.map((msg: string, index: number) => ({
|
||||
id: Date.now() + index, // 简单生成唯一 ID,生产环境建议使用更可靠的 ID 生成器
|
||||
text: msg
|
||||
}))
|
||||
messages.value.push({id: Date.now() + messages.value.length, text: '已完成'})
|
||||
messages.value.reverse()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const connect = (url: string) => {
|
||||
messages.value = []
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
}
|
||||
eventSource = new EventSource(url)
|
||||
// 连接成功
|
||||
eventSource.addEventListener("open", (event) => {
|
||||
console.log('SSE 连接已打开')
|
||||
status.value = 'connected'
|
||||
})
|
||||
// 监听普通消息
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
messages.value.unshift({id: Date.now() + messages.value.length, text: event.data})
|
||||
})
|
||||
// 监听关闭事件
|
||||
eventSource.addEventListener('close', (event) => {
|
||||
messages.value.unshift({id: Date.now() + messages.value.length, text: event.data})
|
||||
closeConnection()
|
||||
})
|
||||
// 错误处理
|
||||
eventSource.addEventListener('error', (event) => {
|
||||
console.log('连接断开或失败', new Date());
|
||||
// 如果这里也频繁触发,确认了是断连重连
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
console.log('状态为 CLOSED');
|
||||
}
|
||||
})
|
||||
}
|
||||
const closeConnection = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
// eventSource = null
|
||||
status.value = 'closed'
|
||||
console.log('SSE 连接已关闭')
|
||||
}
|
||||
}
|
||||
onUnmounted(() => {
|
||||
closeConnection()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-main>
|
||||
<el-card>
|
||||
<template #default>
|
||||
<div v-loading="loading" :style="{height: '100%'}">
|
||||
<el-alert v-for="item in messages" :key="item.id" :title="item.text"
|
||||
type="primary"
|
||||
@close="closeAlert(item.id)"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button-group>
|
||||
<el-button type="primary" v-for="btn in buttons" :key="btn.id"
|
||||
@click="handleClick(btn)">
|
||||
{{ btn.title }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-alert + .el-alert {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-card__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
//height: calc(100vh - 211px + 60px);
|
||||
height: calc(100vh - 211px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
// Extra safety for array and object lookups, but may have false positives.
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Path mapping for cleaner imports.
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||
{
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||
// Bundler mode provides a smoother developer experience.
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||
"types": ["node"],
|
||||
|
||||
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||
"noEmit": true,
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
// import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
// vueDevTools(),
|
||||
AutoImport({
|
||||
resolvers: [
|
||||
ElementPlusResolver()
|
||||
], // 自动导入 Element Plus 相关函数
|
||||
}),
|
||||
Components({
|
||||
resolvers: [
|
||||
ElementPlusResolver()
|
||||
], // 自动注册组件
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://127.0.0.1:5000',
|
||||
// changeOrigin: true,
|
||||
// // rewrite: (path) => path.replace(/^\/api/, '')
|
||||
// // 关键:确保代理不会缓冲响应
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
})
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,24 @@
|
||||
"""手动修改
|
||||
|
||||
Revision ID: 622c1972f4af
|
||||
Revises: b961883c5443
|
||||
Create Date: 2026-03-27 17:22:02.655446
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '622c1972f4af'
|
||||
down_revision = 'b961883c5443'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -0,0 +1,54 @@
|
||||
"""init
|
||||
|
||||
Revision ID: b961883c5443
|
||||
Revises:
|
||||
Create Date: 2026-03-25 19:08:34.121849
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b961883c5443'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('setting',
|
||||
sa.Column('name', sa.String(length=64), nullable=False),
|
||||
sa.Column('mod', sa.String(length=64), nullable=True),
|
||||
sa.Column('fid', sa.Integer(), nullable=False),
|
||||
sa.Column('orderby', sa.String(length=64), nullable=True),
|
||||
sa.Column('baseUrl', sa.String(length=64), nullable=True),
|
||||
sa.Column('targetDate', sa.String(length=64), nullable=True),
|
||||
sa.Column('targetCountDownPage', sa.Integer(), nullable=True),
|
||||
sa.Column('totalPage', sa.Integer(), nullable=True),
|
||||
sa.Column('privilegeLevel', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('fid')
|
||||
)
|
||||
with op.batch_alter_table('setting', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_setting_mod'), ['mod'], unique=True)
|
||||
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=80), nullable=False),
|
||||
sa.Column('email', sa.String(length=120), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('user')
|
||||
with op.batch_alter_table('setting', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_setting_mod'))
|
||||
|
||||
op.drop_table('setting')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,9 @@
|
||||
beautifulsoup4
|
||||
lxml
|
||||
tenacity
|
||||
requests
|
||||
Flask
|
||||
Flask-Migrate
|
||||
Flask-SocketIO
|
||||
Flask-SQLAlchemy
|
||||
Flask-CORS
|
||||
Reference in New Issue
Block a user