|
|
@@ -1,49 +1,99 @@
|
|
|
-from fastapi import FastAPI, Request
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+EH-Downloader 主应用
|
|
|
+"""
|
|
|
+import glob
|
|
|
+import os
|
|
|
+from pathlib import Path
|
|
|
+from typing import List
|
|
|
+
|
|
|
+from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from fastapi.templating import Jinja2Templates
|
|
|
from fastapi.responses import JSONResponse, FileResponse
|
|
|
-import uvicorn
|
|
|
-import glob
|
|
|
-import os
|
|
|
from pydantic import BaseModel
|
|
|
+import uvicorn
|
|
|
+import asyncio
|
|
|
+import threading
|
|
|
+import json
|
|
|
+
|
|
|
+from config import config
|
|
|
+from logger import get_logger
|
|
|
+from realtime_logger import realtime_logger
|
|
|
import step2
|
|
|
-from utils import *
|
|
|
+from utils import run_step1, run_step2
|
|
|
+
|
|
|
+# 设置日志
|
|
|
+logger = get_logger("main", "app.log")
|
|
|
|
|
|
-app = FastAPI(title="EH-Downloader", version="1.0.0")
|
|
|
+app = FastAPI(
|
|
|
+ title=config.app_name,
|
|
|
+ version=config.app_version,
|
|
|
+ description="E-Hentai 画廊下载工具"
|
|
|
+)
|
|
|
|
|
|
-# 在应用启动时检查并创建data文件夹和targets.txt,以及proxy.txt
|
|
|
@app.on_event("startup")
|
|
|
async def startup_event():
|
|
|
- # 检查并创建data文件夹
|
|
|
- data_dir = "data"
|
|
|
- if not os.path.exists(data_dir):
|
|
|
- os.makedirs(data_dir)
|
|
|
- print(f"创建目录: {data_dir}")
|
|
|
+ """应用启动事件"""
|
|
|
+ logger.info(f"启动 {config.app_name} v{config.app_version}")
|
|
|
+ # 注册事件循环到实时日志器,便于跨线程广播
|
|
|
+ try:
|
|
|
+ realtime_logger.set_loop(asyncio.get_running_loop())
|
|
|
+ except RuntimeError:
|
|
|
+ # 若获取失败则忽略
|
|
|
+ pass
|
|
|
|
|
|
- # 检查并创建targets.txt文件
|
|
|
- targets_file = os.path.join(data_dir, "targets.txt")
|
|
|
- if not os.path.exists(targets_file):
|
|
|
- with open(targets_file, 'w', encoding='utf-8') as f:
|
|
|
+ # 确保目录存在
|
|
|
+ config._ensure_directories()
|
|
|
+
|
|
|
+ # 创建默认targets.txt文件
|
|
|
+ if not config.targets_path.exists():
|
|
|
+ with open(config.targets_path, 'w', encoding='utf-8') as f:
|
|
|
f.write("# 在这里添加目标URL,每行一个\n")
|
|
|
f.write("# 示例:\n")
|
|
|
f.write("https://e-hentai.org/g/3550066/47d6393550\n")
|
|
|
- print(f"创建文件: {targets_file}")
|
|
|
- else:
|
|
|
- print(f"文件已存在: {targets_file}")
|
|
|
+ logger.info(f"创建文件: {config.targets_path}")
|
|
|
|
|
|
- # 检查并创建proxy.txt文件
|
|
|
- proxy_file = "proxy.txt"
|
|
|
- if not os.path.exists(proxy_file):
|
|
|
- with open(proxy_file, 'w', encoding='utf-8') as f:
|
|
|
+ # 创建默认proxy.txt文件
|
|
|
+ if not config.proxy_path.exists():
|
|
|
+ with open(config.proxy_path, 'w', encoding='utf-8') as f:
|
|
|
f.write("127.0.0.1:7890\n")
|
|
|
- print(f"创建文件: {proxy_file}")
|
|
|
- else:
|
|
|
- print(f"文件已存在: {proxy_file}")
|
|
|
+ logger.info(f"创建文件: {config.proxy_path}")
|
|
|
+
|
|
|
+ logger.info("应用启动完成")
|
|
|
|
|
|
# 挂载静态文件和模板
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
+# WebSocket 路由
|
|
|
+@app.websocket("/ws")
|
|
|
+async def websocket_endpoint(websocket: WebSocket):
|
|
|
+ """WebSocket连接处理"""
|
|
|
+ await websocket.accept()
|
|
|
+ realtime_logger.add_connection(websocket)
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 发送最近的日志
|
|
|
+ recent_logs = await realtime_logger.get_recent_logs(20)
|
|
|
+ for log_entry in recent_logs:
|
|
|
+ await websocket.send_text(json.dumps(log_entry, ensure_ascii=False))
|
|
|
+
|
|
|
+ # 保持连接
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ # 等待客户端消息(心跳检测)
|
|
|
+ data = await websocket.receive_text()
|
|
|
+ if data == "ping":
|
|
|
+ await websocket.send_text("pong")
|
|
|
+ except WebSocketDisconnect:
|
|
|
+ break
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"WebSocket错误: {e}")
|
|
|
+ finally:
|
|
|
+ realtime_logger.remove_connection(websocket)
|
|
|
+
|
|
|
# favicon 路由
|
|
|
@app.get("/favicon.ico", include_in_schema=False)
|
|
|
async def favicon():
|
|
|
@@ -52,37 +102,22 @@ async def favicon():
|
|
|
@app.get("/")
|
|
|
async def home(request: Request):
|
|
|
"""主页面"""
|
|
|
- # 读取proxy.txt中的代理列表
|
|
|
- proxies = []
|
|
|
try:
|
|
|
- with open("proxy.txt", 'r', encoding='utf-8') as f:
|
|
|
- proxies = [line.strip() for line in f.readlines() if line.strip()]
|
|
|
+ proxies = config.get_proxies()
|
|
|
+ return templates.TemplateResponse("index.html", {
|
|
|
+ "request": request,
|
|
|
+ "proxies": proxies,
|
|
|
+ "default_proxy": proxies[0] if proxies else "127.0.0.1:7890"
|
|
|
+ })
|
|
|
except Exception as e:
|
|
|
- print(f"读取proxy.txt失败: {e}")
|
|
|
- proxies = ["127.0.0.1:7890"]
|
|
|
-
|
|
|
- # 如果没有代理配置,使用默认值
|
|
|
- if not proxies:
|
|
|
- proxies = ["127.0.0.1:7890"]
|
|
|
-
|
|
|
- return templates.TemplateResponse("index.html", {
|
|
|
- "request": request,
|
|
|
- "proxies": proxies,
|
|
|
- "default_proxy": proxies[0] if proxies else "127.0.0.1:7890"
|
|
|
- })
|
|
|
+ logger.error(f"渲染主页失败: {e}")
|
|
|
+ raise HTTPException(status_code=500, detail="服务器内部错误")
|
|
|
|
|
|
@app.post("/load_urls")
|
|
|
async def load_urls():
|
|
|
"""读取 targets.txt 文件中的URL"""
|
|
|
try:
|
|
|
- file_path = "data/targets.txt"
|
|
|
-
|
|
|
- # 读取文件内容
|
|
|
- with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
- urls = [line.strip() for line in f.readlines() if line.strip()]
|
|
|
-
|
|
|
- # 过滤掉空行和注释行(以#开头的行)
|
|
|
- urls = [url for url in urls if url and not url.startswith('#')]
|
|
|
+ urls = config.get_targets()
|
|
|
|
|
|
if not urls:
|
|
|
return JSONResponse({
|
|
|
@@ -91,6 +126,7 @@ async def load_urls():
|
|
|
"urls": []
|
|
|
})
|
|
|
|
|
|
+ logger.info(f"成功读取 {len(urls)} 个URL")
|
|
|
return JSONResponse({
|
|
|
"success": True,
|
|
|
"message": f"成功读取 {len(urls)} 个URL",
|
|
|
@@ -98,6 +134,7 @@ async def load_urls():
|
|
|
})
|
|
|
|
|
|
except Exception as e:
|
|
|
+ logger.error(f"读取URL失败: {e}")
|
|
|
return JSONResponse({
|
|
|
"success": False,
|
|
|
"message": f"读取文件时出错: {str(e)}",
|
|
|
@@ -118,25 +155,77 @@ class ProxyRequest(BaseModel):
|
|
|
|
|
|
@app.post("/download_urls")
|
|
|
async def download_urls(req: ProxyRequest):
|
|
|
- # 解析proxy字符串为ip和port
|
|
|
- if ":" in req.proxy:
|
|
|
- ip, port = req.proxy.split(":", 1)
|
|
|
- proxy = f"http://{ip}:{port}"
|
|
|
- else:
|
|
|
- proxy = None
|
|
|
- msg = await run_step1(proxy)
|
|
|
- return JSONResponse({"success": True, "message": msg})
|
|
|
+ """下载画廊链接"""
|
|
|
+ try:
|
|
|
+ # 解析proxy字符串为ip和port
|
|
|
+ if ":" in req.proxy:
|
|
|
+ ip, port = req.proxy.split(":", 1)
|
|
|
+ proxy = f"http://{ip}:{port}"
|
|
|
+ else:
|
|
|
+ proxy = None
|
|
|
+
|
|
|
+ # 发送实时日志
|
|
|
+ await realtime_logger.broadcast_log(f"开始抓取画廊链接,代理: {proxy}", "INFO", "step1")
|
|
|
+
|
|
|
+ # 在后台线程中执行,避免阻塞
|
|
|
+ def run_step1_sync():
|
|
|
+ import asyncio
|
|
|
+ loop = asyncio.new_event_loop()
|
|
|
+ asyncio.set_event_loop(loop)
|
|
|
+ try:
|
|
|
+ return loop.run_until_complete(run_step1(proxy))
|
|
|
+ finally:
|
|
|
+ loop.close()
|
|
|
+
|
|
|
+ # 使用线程池执行
|
|
|
+ import concurrent.futures
|
|
|
+ with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
|
+ future = executor.submit(run_step1_sync)
|
|
|
+ msg = future.result()
|
|
|
+
|
|
|
+ await realtime_logger.broadcast_log(f"画廊链接抓取完成: {msg}", "SUCCESS", "step1")
|
|
|
+ return JSONResponse({"success": True, "message": msg})
|
|
|
+ except Exception as e:
|
|
|
+ await realtime_logger.broadcast_log(f"抓取画廊链接失败: {e}", "ERROR", "step1")
|
|
|
+ logger.error(f"抓取画廊链接失败: {e}")
|
|
|
+ return JSONResponse({"success": False, "message": f"抓取失败: {str(e)}"})
|
|
|
|
|
|
@app.post("/download_images")
|
|
|
async def download_images(req: ProxyRequest):
|
|
|
- # 解析proxy字符串为ip和port
|
|
|
- if ":" in req.proxy:
|
|
|
- ip, port = req.proxy.split(":", 1)
|
|
|
- proxy = f"http://{ip}:{port}"
|
|
|
- else:
|
|
|
- proxy = None
|
|
|
- msg = await run_step2(proxy)
|
|
|
- return JSONResponse({"success": True, "message": msg})
|
|
|
+ """下载图片"""
|
|
|
+ try:
|
|
|
+ # 解析proxy字符串为ip和port
|
|
|
+ if ":" in req.proxy:
|
|
|
+ ip, port = req.proxy.split(":", 1)
|
|
|
+ proxy = f"http://{ip}:{port}"
|
|
|
+ else:
|
|
|
+ proxy = None
|
|
|
+
|
|
|
+ # 发送实时日志
|
|
|
+ await realtime_logger.broadcast_log(f"开始下载图片,代理: {proxy}", "INFO", "step2")
|
|
|
+
|
|
|
+ # 在后台线程中执行,避免阻塞
|
|
|
+ def run_step2_sync():
|
|
|
+ import asyncio
|
|
|
+ loop = asyncio.new_event_loop()
|
|
|
+ asyncio.set_event_loop(loop)
|
|
|
+ try:
|
|
|
+ return loop.run_until_complete(run_step2(proxy))
|
|
|
+ finally:
|
|
|
+ loop.close()
|
|
|
+
|
|
|
+ # 使用线程池执行
|
|
|
+ import concurrent.futures
|
|
|
+ with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
|
+ future = executor.submit(run_step2_sync)
|
|
|
+ msg = future.result()
|
|
|
+
|
|
|
+ await realtime_logger.broadcast_log(f"图片下载完成: {msg}", "SUCCESS", "step2")
|
|
|
+ return JSONResponse({"success": True, "message": msg})
|
|
|
+ except Exception as e:
|
|
|
+ await realtime_logger.broadcast_log(f"下载图片失败: {e}", "ERROR", "step2")
|
|
|
+ logger.error(f"下载图片失败: {e}")
|
|
|
+ return JSONResponse({"success": False, "message": f"下载失败: {str(e)}"})
|
|
|
|
|
|
@app.post("/clean_files")
|
|
|
async def clean_files():
|
|
|
@@ -145,24 +234,23 @@ async def clean_files():
|
|
|
deleted_files = []
|
|
|
error_files = []
|
|
|
|
|
|
- # 查找当前目录及所有子目录中的 .log 和 .json 文件
|
|
|
- patterns = ["**/*.log", "**/*.json"]
|
|
|
-
|
|
|
- for pattern in patterns:
|
|
|
+ # 使用配置中的清理模式
|
|
|
+ for pattern in config.cleanup_patterns:
|
|
|
for file_path in glob.glob(pattern, recursive=True):
|
|
|
try:
|
|
|
- # 跳过 data/targets.txt 文件,因为这是配置文件
|
|
|
- if file_path == "data/targets.txt":
|
|
|
+ # 跳过排除的文件
|
|
|
+ if file_path in config.cleanup_exclude:
|
|
|
continue
|
|
|
|
|
|
os.remove(file_path)
|
|
|
deleted_files.append(file_path)
|
|
|
- print(f"已删除文件: {file_path}")
|
|
|
+ logger.info(f"已删除文件: {file_path}")
|
|
|
except Exception as e:
|
|
|
error_files.append(f"{file_path}: {str(e)}")
|
|
|
- print(f"删除文件失败 {file_path}: {str(e)}")
|
|
|
+ logger.error(f"删除文件失败 {file_path}: {str(e)}")
|
|
|
|
|
|
if error_files:
|
|
|
+ logger.warning(f"清理完成,但部分文件删除失败: {len(error_files)} 个")
|
|
|
return JSONResponse({
|
|
|
"success": False,
|
|
|
"message": f"清理完成,但部分文件删除失败",
|
|
|
@@ -172,6 +260,7 @@ async def clean_files():
|
|
|
"error_files": error_files
|
|
|
})
|
|
|
else:
|
|
|
+ logger.info(f"成功清理 {len(deleted_files)} 个文件")
|
|
|
return JSONResponse({
|
|
|
"success": True,
|
|
|
"message": f"成功清理 {len(deleted_files)} 个文件",
|
|
|
@@ -181,6 +270,7 @@ async def clean_files():
|
|
|
})
|
|
|
|
|
|
except Exception as e:
|
|
|
+ logger.error(f"清理过程中出错: {e}")
|
|
|
return JSONResponse({
|
|
|
"success": False,
|
|
|
"message": f"清理过程中出错: {str(e)}",
|
|
|
@@ -190,14 +280,26 @@ async def clean_files():
|
|
|
|
|
|
@app.post("/check_incomplete")
|
|
|
async def check_incomplete():
|
|
|
- result = await step2.scan_tasks()
|
|
|
-
|
|
|
"""检查未完成文件"""
|
|
|
- return JSONResponse({
|
|
|
- "success": True,
|
|
|
- "message": "检查未完成文件功能已就绪",
|
|
|
- "data": f"共 {len(result)} 个文件未下载"
|
|
|
- })
|
|
|
+ try:
|
|
|
+ result = await step2.scan_tasks()
|
|
|
+ logger.info(f"检查未完成文件: {len(result)} 个")
|
|
|
+ return JSONResponse({
|
|
|
+ "success": True,
|
|
|
+ "message": "检查未完成文件功能已就绪",
|
|
|
+ "data": f"共 {len(result)} 个文件未下载"
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"检查未完成文件失败: {e}")
|
|
|
+ return JSONResponse({
|
|
|
+ "success": False,
|
|
|
+ "message": f"检查失败: {str(e)}"
|
|
|
+ })
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
|
|
+ uvicorn.run(
|
|
|
+ "main:app",
|
|
|
+ host=config.host,
|
|
|
+ port=config.port,
|
|
|
+ reload=config.debug
|
|
|
+ )
|