main
Jack 2 months ago
parent e95e9d000b
commit 68c1c143a4
  1. 571
      main.py
  2. 11
      post_eh_data.js

@ -26,8 +26,8 @@ logger = logging.getLogger(__name__)
DOWNLOADS_DIR = "downloads" DOWNLOADS_DIR = "downloads"
MAX_FILENAME_LENGTH = 100 MAX_FILENAME_LENGTH = 100
INVALID_FILENAME_CHARS = '<>:"/\\|?*' INVALID_FILENAME_CHARS = '<>:"/\\|?*'
MAX_CONCURRENT_DOWNLOADS = 5 # 最大并发下载数 MAX_CONCURRENT_DOWNLOADS = 5
DOWNLOAD_TIMEOUT = 30 # 下载超时时间(秒) DOWNLOAD_TIMEOUT = 30
# FastAPI应用 # FastAPI应用
app = FastAPI(title="eh-v2") app = FastAPI(title="eh-v2")
@ -57,39 +57,29 @@ class DownloadStatusResponse(BaseModel):
# 工具函数 # 工具函数
def setup_downloads_directory() -> Path: def setup_downloads_directory() -> Path:
"""创建并返回下载目录路径"""
downloads_path = Path(DOWNLOADS_DIR) downloads_path = Path(DOWNLOADS_DIR)
downloads_path.mkdir(exist_ok=True) downloads_path.mkdir(exist_ok=True)
logger.info(f"下载目录已准备: {downloads_path.absolute()}")
return downloads_path return downloads_path
def sanitize_filename(filename: str) -> str: def sanitize_filename(filename: str) -> str:
"""清理文件名,移除非法字符并限制长度"""
sanitized = filename sanitized = filename
for char in INVALID_FILENAME_CHARS: for char in INVALID_FILENAME_CHARS:
sanitized = sanitized.replace(char, '_') sanitized = sanitized.replace(char, '_')
# 限制文件名长度
if len(sanitized) > MAX_FILENAME_LENGTH: if len(sanitized) > MAX_FILENAME_LENGTH:
sanitized = sanitized[:MAX_FILENAME_LENGTH] sanitized = sanitized[:MAX_FILENAME_LENGTH]
return sanitized return sanitized
def create_title_directory(base_path: Path, title: str) -> Path: def create_title_directory(base_path: Path, title: str) -> Path:
"""创建标题对应的目录"""
safe_title = sanitize_filename(title) safe_title = sanitize_filename(title)
title_dir = base_path / safe_title title_dir = base_path / safe_title
title_dir.mkdir(exist_ok=True) title_dir.mkdir(exist_ok=True)
logger.info(f"创建标题目录: {title_dir}")
return title_dir return title_dir
async def save_data_to_file(file_path: Path, data: Dict[str, Any]) -> None: async def save_data_to_file(file_path: Path, data: Dict[str, Any]) -> None:
"""异步保存数据到JSON文件"""
async with aiofiles.open(file_path, 'w', encoding='utf-8') as f: async with aiofiles.open(file_path, 'w', encoding='utf-8') as f:
await f.write(json.dumps(data, ensure_ascii=False, indent=2)) await f.write(json.dumps(data, ensure_ascii=False, indent=2))
def get_all_galleries() -> List[GalleryInfo]: def get_all_galleries() -> List[GalleryInfo]:
"""获取所有画廊信息"""
galleries = [] galleries = []
downloads_path = Path(DOWNLOADS_DIR) downloads_path = Path(DOWNLOADS_DIR)
@ -104,7 +94,6 @@ def get_all_galleries() -> List[GalleryInfo]:
with open(data_file, 'r', encoding='utf-8') as f: with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.load(f)
# 计算已下载的图片数量
downloaded_count = 0 downloaded_count = 0
if 'all_images' in data: if 'all_images' in data:
for filename, url in data['all_images'].items(): for filename, url in data['all_images'].items():
@ -124,38 +113,32 @@ def get_all_galleries() -> List[GalleryInfo]:
return galleries return galleries
async def download_single_image(client: httpx.AsyncClient, url: str, file_path: Path, semaphore: asyncio.Semaphore) -> bool: async def download_single_image(client: httpx.AsyncClient, url: str, file_path: Path, semaphore: asyncio.Semaphore) -> bool:
"""下载单张图片 - 精简版"""
async with semaphore: async with semaphore:
try: try:
logger.info(f"开始下载: {url}") # 先获取图片后缀
if file_path.exists():
logger.info(f"文件已存在: {file_path}")
return True
# 第一步:获取中间页面
response = await client.get(url, timeout=DOWNLOAD_TIMEOUT) response = await client.get(url, timeout=DOWNLOAD_TIMEOUT)
response.raise_for_status() response.raise_for_status()
# 第二步:提取真实图片URL
import re import re
match = re.search(r'img id="img" src="(.*?)"', response.text) match = re.search(r'img id="img" src="(.*?)"', response.text)
if not match: if not match:
logger.error(f"无法提取图片URL: {url}")
return False return False
real_img_url = match.group(1) real_img_url = match.group(1)
logger.info(f"真实URL: {real_img_url}") suffix = real_img_url.split('.')[-1]
# 创建带后缀的文件路径
file_path_with_suffix = file_path.with_suffix('.' + suffix)
if file_path_with_suffix.exists():
return True
# 第三步:下载图片
img_response = await client.get(real_img_url, timeout=DOWNLOAD_TIMEOUT) img_response = await client.get(real_img_url, timeout=DOWNLOAD_TIMEOUT)
img_response.raise_for_status() img_response.raise_for_status()
# 保存图片 async with aiofiles.open(file_path_with_suffix, 'wb') as f:
async with aiofiles.open(file_path, 'wb') as f:
await f.write(img_response.content) await f.write(img_response.content)
logger.info(f"下载完成: {file_path}")
return True return True
except Exception as e: except Exception as e:
@ -163,7 +146,6 @@ async def download_single_image(client: httpx.AsyncClient, url: str, file_path:
return False return False
async def download_gallery_images(title: str) -> DownloadStatusResponse: async def download_gallery_images(title: str) -> DownloadStatusResponse:
"""下载指定画廊的所有图片"""
safe_title = sanitize_filename(title) safe_title = sanitize_filename(title)
gallery_path = downloads_path / safe_title gallery_path = downloads_path / safe_title
data_file = gallery_path / "data.json" data_file = gallery_path / "data.json"
@ -178,7 +160,6 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
) )
try: try:
# 读取画廊数据
async with aiofiles.open(data_file, 'r', encoding='utf-8') as f: async with aiofiles.open(data_file, 'r', encoding='utf-8') as f:
content = await f.read() content = await f.read()
data = json.loads(content) data = json.loads(content)
@ -195,7 +176,6 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
current_progress=0.0 current_progress=0.0
) )
# 初始化下载状态
download_status[title] = { download_status[title] = {
"downloaded": 0, "downloaded": 0,
"total": total_images, "total": total_images,
@ -204,10 +184,8 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
logger.info(f"开始下载画廊 '{title}',共 {total_images} 张图片") logger.info(f"开始下载画廊 '{title}',共 {total_images} 张图片")
# 创建信号量限制并发数
semaphore = asyncio.Semaphore(MAX_CONCURRENT_DOWNLOADS) semaphore = asyncio.Semaphore(MAX_CONCURRENT_DOWNLOADS)
# 使用异步HTTP客户端
async with httpx.AsyncClient( async with httpx.AsyncClient(
headers={ headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
@ -215,12 +193,10 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
follow_redirects=True follow_redirects=True
) as client: ) as client:
# 准备下载任务
tasks = [] tasks = []
for filename, url in all_images.items(): for filename, url in all_images.items():
image_path = gallery_path / filename image_path = gallery_path / filename
# 如果图片已存在,跳过下载但计入完成数量
if image_path.exists(): if image_path.exists():
download_status[title]["downloaded"] += 1 download_status[title]["downloaded"] += 1
continue continue
@ -228,26 +204,20 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
task = download_single_image(client, url, image_path, semaphore) task = download_single_image(client, url, image_path, semaphore)
tasks.append(task) tasks.append(task)
# 批量执行下载任务
if tasks: if tasks:
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks, return_exceptions=True)
# 统计成功下载的数量
successful_downloads = sum(1 for result in results if result is True) successful_downloads = sum(1 for result in results if result is True)
download_status[title]["downloaded"] += successful_downloads download_status[title]["downloaded"] += successful_downloads
# 更新最终状态
downloaded_count = download_status[title]["downloaded"] downloaded_count = download_status[title]["downloaded"]
progress = (downloaded_count / total_images) * 100 progress = (downloaded_count / total_images) * 100
if downloaded_count == total_images: if downloaded_count == total_images:
download_status[title]["status"] = "completed" download_status[title]["status"] = "completed"
message = f"下载完成!共下载 {downloaded_count}/{total_images} 张图片" message = f"下载完成!共下载 {downloaded_count}/{total_images} 张图片"
logger.info(f"画廊 '{title}' {message}")
else: else:
download_status[title]["status"] = "partial" download_status[title]["status"] = "partial"
message = f"部分完成!下载 {downloaded_count}/{total_images} 张图片" message = f"部分完成!下载 {downloaded_count}/{total_images} 张图片"
logger.warning(f"画廊 '{title}' {message}")
return DownloadStatusResponse( return DownloadStatusResponse(
status="success", status="success",
@ -272,28 +242,25 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
) )
async def download_all_pending_galleries(): async def download_all_pending_galleries():
"""下载所有未完成的画廊"""
galleries = get_all_galleries() galleries = get_all_galleries()
pending_galleries = [g for g in galleries if g.downloaded_images < g.total_images] pending_galleries = [g for g in galleries if g.downloaded_images < g.total_images]
logger.info(f"找到 {len(pending_galleries)} 个待下载画廊")
if not pending_galleries: if not pending_galleries:
logger.info("没有待下载的画廊") logger.info("没有待下载的画廊")
return return
logger.info(f"开始批量下载 {len(pending_galleries)} 个画廊")
for gallery in pending_galleries: for gallery in pending_galleries:
if gallery.downloaded_images < gallery.total_images: logger.info(f"开始下载画廊: {gallery.title}")
logger.info(f"开始下载画廊: {gallery.title}") result = await download_gallery_images(gallery.title)
result = await download_gallery_images(gallery.title)
if result.status == "success": if result.status == "success":
logger.info(f"画廊 '{gallery.title}' 下载完成: {result.message}") logger.info(f"画廊 '{gallery.title}' 下载完成: {result.message}")
else: else:
logger.error(f"画廊 '{gallery.title}' 下载失败: {result.message}") logger.error(f"画廊 '{gallery.title}' 下载失败: {result.message}")
# 添加延迟避免请求过于频繁 await asyncio.sleep(1)
await asyncio.sleep(1)
logger.info("批量下载任务完成") logger.info("批量下载任务完成")
@ -301,9 +268,35 @@ async def download_all_pending_galleries():
downloads_path = setup_downloads_directory() downloads_path = setup_downloads_directory()
# API路由 # API路由
@app.post("/save_url")
@app.options("/save_url")
async def save_url_data(request: SaveDataRequest = None):
if not request:
return {"status": "ok"}
try:
title_dir = create_title_directory(downloads_path, request.title)
data_file = title_dir / "data.json"
await save_data_to_file(data_file, {
"url": request.url,
"title": request.title,
"all_images": request.all_images,
"total_images": request.total_images
})
logger.info(f"成功保存数据: {request.title}")
return {
"status": "success",
"message": f"数据保存成功,共 {request.total_images} 张图片",
"path": str(title_dir)
}
except Exception as e:
logger.error(f"保存数据失败: {e}")
raise HTTPException(status_code=500, detail=f"保存失败: {str(e)}")
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def read_gallery_manager(): async def read_gallery_manager():
"""画廊管理页面"""
return """ return """
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
@ -312,193 +305,24 @@ async def read_gallery_manager():
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画廊下载管理器</title> <title>画廊下载管理器</title>
<style> <style>
* { * { margin: 0; padding: 0; box-sizing: border-box; }
margin: 0; body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
padding: 0; .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); overflow: hidden; }
box-sizing: border-box; .header { background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 30px; text-align: center; }
} .header h1 { font-size: 2.5em; margin-bottom: 10px; }
body { .controls { padding: 20px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; display: flex; gap: 15px; flex-wrap: wrap; }
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; }
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); .btn-primary { background: #007bff; color: white; }
min-height: 100vh; .btn-primary:hover { background: #0056b3; }
padding: 20px; .btn-success { background: #28a745; color: white; }
} .btn-success:hover { background: #1e7e34; }
.container { .gallery-list { padding: 20px; }
max-width: 1200px; .gallery-item { background: white; border: 1px solid #e9ecef; border-radius: 10px; padding: 20px; margin-bottom: 15px; }
margin: 0 auto; .gallery-title { font-size: 1.3em; font-weight: 600; color: #2c3e50; margin-bottom: 8px; }
background: white; .gallery-stats { display: flex; gap: 20px; color: #6c757d; font-size: 0.9em; }
border-radius: 15px; .progress-bar { width: 100%; height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden; margin-top: 8px; }
box-shadow: 0 20px 40px rgba(0,0,0,0.1); .progress-fill { height: 100%; background: linear-gradient(90deg, #28a745, #20c997); transition: width 0.3s ease; }
overflow: hidden; .empty-state { text-align: center; padding: 60px 20px; color: #6c757d; }
}
.header {
background: linear-gradient(135deg, #2c3e50, #34495e);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.8;
font-size: 1.1em;
}
.controls {
padding: 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
transform: translateY(-2px);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #1e7e34;
transform: translateY(-2px);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-2px);
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-2px);
}
.btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
.gallery-list {
padding: 20px;
}
.gallery-item {
background: white;
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
display: flex;
justify-content: between;
align-items: center;
}
.gallery-item:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.gallery-info {
flex: 1;
}
.gallery-title {
font-size: 1.3em;
font-weight: 600;
color: #2c3e50;
margin-bottom: 8px;
}
.gallery-stats {
display: flex;
gap: 20px;
color: #6c757d;
font-size: 0.9em;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
transition: width 0.3s ease;
}
.completed .progress-fill {
background: linear-gradient(90deg, #007bff, #0056b3);
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
margin-left: 10px;
}
.status-downloading {
background: #fff3cd;
color: #856404;
}
.status-completed {
background: #d1ecf1;
color: #0c5460;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-state h3 {
margin-bottom: 10px;
font-size: 1.5em;
}
.stats-summary {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
color: #6c757d;
}
.gallery-actions {
display: flex;
gap: 10px;
}
.download-progress {
margin-top: 10px;
font-size: 0.9em;
color: #6c757d;
}
</style> </style>
</head> </head>
<body> <body>
@ -508,25 +332,9 @@ async def read_gallery_manager():
<p>管理您的画廊下载任务</p> <p>管理您的画廊下载任务</p>
</div> </div>
<div class="stats-summary" id="statsSummary">
<span>总计: <strong id="totalGalleries">0</strong> 个画廊</span>
<span>待下载: <strong id="pendingGalleries">0</strong> </span>
<span>已完成: <strong id="completedGalleries">0</strong> </span>
</div>
<div class="controls"> <div class="controls">
<button class="btn btn-primary" onclick="loadGalleries()"> <button class="btn btn-primary" onclick="loadGalleries()">📁 读取文件夹</button>
📁 读取文件夹 <button class="btn btn-success" onclick="startDownload()" id="downloadBtn"> 开始下载所有未完成</button>
</button>
<button class="btn btn-success" onclick="startDownload()" id="downloadBtn">
开始下载所有未完成
</button>
<button class="btn btn-warning" onclick="downloadSelected()" id="downloadSelectedBtn">
🎯 下载选中画廊
</button>
<button class="btn btn-danger" onclick="deleteJsonFiles()">
🗑 删除所有JSON文件
</button>
</div> </div>
<div class="gallery-list" id="galleryList"> <div class="gallery-list" id="galleryList">
@ -538,16 +346,11 @@ async def read_gallery_manager():
</div> </div>
<script> <script>
let currentGalleries = [];
let selectedGalleries = new Set();
async function loadGalleries() { async function loadGalleries() {
try { try {
const response = await fetch('/api/galleries'); const response = await fetch('/api/galleries');
const galleries = await response.json(); const galleries = await response.json();
currentGalleries = galleries;
displayGalleries(galleries); displayGalleries(galleries);
updateStats(galleries);
} catch (error) { } catch (error) {
alert('读取文件夹失败: ' + error); alert('读取文件夹失败: ' + error);
} }
@ -557,102 +360,47 @@ async def read_gallery_manager():
const galleryList = document.getElementById('galleryList'); const galleryList = document.getElementById('galleryList');
if (galleries.length === 0) { if (galleries.length === 0) {
galleryList.innerHTML = ` galleryList.innerHTML = '<div class="empty-state"><h3>暂无画廊数据</h3></div>';
<div class="empty-state">
<h3>暂无画廊数据</h3>
<p>请先添加画廊数据文件</p>
</div>
`;
return; return;
} }
// 过滤掉已完成的画廊已下载数量等于总数量 const pendingGalleries = galleries.filter(gallery => gallery.downloaded_images < gallery.total_images);
const pendingGalleries = galleries.filter(gallery =>
gallery.downloaded_images < gallery.total_images
);
if (pendingGalleries.length === 0) { if (pendingGalleries.length === 0) {
galleryList.innerHTML = ` galleryList.innerHTML = '<div class="empty-state"><h3>🎉 所有任务已完成!</h3></div>';
<div class="empty-state">
<h3>🎉 所有任务已完成</h3>
<p>没有待下载的画廊任务</p>
</div>
`;
return; return;
} }
galleryList.innerHTML = pendingGalleries.map(gallery => { galleryList.innerHTML = pendingGalleries.map(gallery => {
const progress = (gallery.downloaded_images / gallery.total_images) * 100; const progress = (gallery.downloaded_images / gallery.total_images) * 100;
const isCompleted = gallery.downloaded_images === gallery.total_images;
const isSelected = selectedGalleries.has(gallery.title);
return ` return `
<div class="gallery-item ${isCompleted ? 'completed' : ''} ${isSelected ? 'selected' : ''}" <div class="gallery-item">
onclick="toggleGallerySelection('${gallery.title}')" <div class="gallery-title">${gallery.title}</div>
style="cursor: pointer; ${isSelected ? 'border-color: #007bff; background-color: #f8f9fa;' : ''}"> <div class="gallery-stats">
<div class="gallery-info"> <span>总图片: ${gallery.total_images}</span>
<div class="gallery-title"> <span>已下载: ${gallery.downloaded_images}</span>
<input type="checkbox" ${isSelected ? 'checked' : ''} <span>进度: ${Math.round(progress)}%</span>
onclick="event.stopPropagation(); toggleGallerySelection('${gallery.title}')"> </div>
${gallery.title} <div class="progress-bar">
${isCompleted ? <div class="progress-fill" style="width: ${progress}%"></div>
'<span class="status-badge status-completed">已完成</span>' :
'<span class="status-badge status-downloading">待下载</span>'
}
</div>
<div class="gallery-stats">
<span>总图片: ${gallery.total_images}</span>
<span>已下载: ${gallery.downloaded_images}</span>
<span>进度: ${Math.round(progress)}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<div class="gallery-actions">
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadSingleGallery('${gallery.title}')">
单独下载
</button>
</div>
</div> </div>
</div> </div>
`; `;
}).join(''); }).join('');
} }
function toggleGallerySelection(title) {
if (selectedGalleries.has(title)) {
selectedGalleries.delete(title);
} else {
selectedGalleries.add(title);
}
displayGalleries(currentGalleries);
}
function updateStats(galleries) {
const total = galleries.length;
const completed = galleries.filter(g => g.downloaded_images === g.total_images).length;
const pending = total - completed;
document.getElementById('totalGalleries').textContent = total;
document.getElementById('pendingGalleries').textContent = pending;
document.getElementById('completedGalleries').textContent = completed;
}
async function startDownload() { async function startDownload() {
const btn = document.getElementById('downloadBtn'); const btn = document.getElementById('downloadBtn');
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '⏳ 下载中...'; btn.innerHTML = '⏳ 下载中...';
try { try {
const response = await fetch('/api/download/all', { const response = await fetch('/api/download/all', { method: 'POST' });
method: 'POST'
});
const result = await response.json(); const result = await response.json();
if (result.status === 'success') { if (result.status === 'success') {
alert('批量下载任务已开始!请查看控制台了解进度。'); alert('批量下载任务已开始!请查看后端控制台了解进度。');
// 定期刷新状态 setTimeout(loadGalleries, 5000);
setTimeout(loadGalleries, 3000);
} else { } else {
alert('下载失败: ' + result.message); alert('下载失败: ' + result.message);
} }
@ -664,71 +412,6 @@ async def read_gallery_manager():
} }
} }
async function downloadSelected() {
if (selectedGalleries.size === 0) {
alert('请先选择要下载的画廊!');
return;
}
const btn = document.getElementById('downloadSelectedBtn');
btn.disabled = true;
btn.innerHTML = '⏳ 下载中...';
try {
for (const title of selectedGalleries) {
await downloadSingleGallery(title);
// 添加延迟避免请求过于频繁
await new Promise(resolve => setTimeout(resolve, 1000));
}
alert('选中的画廊下载任务已完成!');
selectedGalleries.clear();
await loadGalleries();
} catch (error) {
alert('下载失败: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = '🎯 下载选中画廊';
}
}
async function downloadSingleGallery(title) {
try {
const response = await fetch(`/api/download/${encodeURIComponent(title)}`, {
method: 'POST'
});
const result = await response.json();
if (result.status === 'success') {
console.log(`开始下载: ${title}`);
alert(`开始下载: ${title}`);
// 刷新状态
setTimeout(loadGalleries, 2000);
} else {
alert(`下载失败: ${result.message}`);
}
} catch (error) {
alert('下载请求失败: ' + error);
}
}
async function deleteJsonFiles() {
if (!confirm('确定要删除所有JSON文件吗?此操作不可恢复!')) {
return;
}
try {
const response = await fetch('/api/cleanup', {
method: 'DELETE'
});
const result = await response.json();
alert(result.message);
await loadGalleries(); // 刷新列表
} catch (error) {
alert('删除失败: ' + error);
}
}
// 页面加载时自动读取
document.addEventListener('DOMContentLoaded', loadGalleries); document.addEventListener('DOMContentLoaded', loadGalleries);
</script> </script>
</body> </body>
@ -737,92 +420,28 @@ async def read_gallery_manager():
@app.get("/api/galleries") @app.get("/api/galleries")
async def get_galleries(): async def get_galleries():
"""获取所有画廊信息(包括已完成和未完成的)"""
galleries = get_all_galleries() galleries = get_all_galleries()
return galleries return galleries
@app.post("/api/download/{title}")
async def download_gallery(title: str, background_tasks: BackgroundTasks):
"""开始下载指定画廊的图片"""
try:
# 使用后台任务执行下载,避免阻塞请求
background_tasks.add_task(download_gallery_images, title)
return {
"status": "success",
"message": f"开始下载画廊: {title}",
"title": title
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"下载失败: {str(e)}")
@app.post("/api/download/all") @app.post("/api/download/all")
async def download_all_galleries(background_tasks: BackgroundTasks): async def download_all_galleries(background_tasks: BackgroundTasks):
"""开始下载所有未完成的画廊""" background_tasks.add_task(download_all_pending_galleries)
try: return {
# 使用后台任务执行批量下载 "status": "success",
background_tasks.add_task(download_all_pending_galleries) "message": "开始批量下载所有未完成的画廊"
}
return { @app.post("/api/download/{title}")
"status": "success", async def download_gallery(title: str, background_tasks: BackgroundTasks):
"message": "开始批量下载所有未完成的画廊" background_tasks.add_task(download_gallery_images, title)
} return {
except Exception as e: "status": "success",
raise HTTPException(status_code=500, detail=f"批量下载失败: {str(e)}") "message": f"开始下载画廊: {title}",
"title": title
@app.get("/api/download/status/{title}") }
async def get_download_status(title: str):
"""获取指定画廊的下载状态"""
status = download_status.get(title, {})
return status
@app.delete("/api/cleanup")
async def cleanup_json_files():
"""删除所有JSON文件(保留图片)"""
try:
deleted_count = 0
downloads_path = Path(DOWNLOADS_DIR)
for gallery_dir in downloads_path.iterdir():
if gallery_dir.is_dir():
data_file = gallery_dir / "data.json"
if data_file.exists():
data_file.unlink()
deleted_count += 1
return {
"status": "success",
"message": f"已删除 {deleted_count} 个JSON文件",
"deleted_count": deleted_count
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"清理失败: {str(e)}")
@app.delete("/api/galleries/{title}")
async def delete_gallery(title: str):
"""删除指定画廊的所有文件"""
try:
safe_title = sanitize_filename(title)
gallery_path = downloads_path / safe_title
if gallery_path.exists():
# 删除整个画廊目录
import shutil
shutil.rmtree(gallery_path)
# 清除下载状态
download_status.pop(title, None)
return {
"status": "success",
"message": f"已删除画廊: {title}"
}
else:
raise HTTPException(status_code=404, detail="画廊不存在")
except Exception as e:
raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}")
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""健康检查端点"""
return {"status": "healthy"} return {"status": "healthy"}
if __name__ == "__main__": if __name__ == "__main__":

@ -14,7 +14,7 @@
// 全局配置 - 请根据实际情况修改这些值 // 全局配置 - 请根据实际情况修改这些值
const BACKEND_IP = '127.0.0.1'; const BACKEND_IP = '127.0.0.1';
const BACKEND_PORT = '5100'; const BACKEND_PORT = '5100';
const BUTTON_LOCATION_SELECTOR = 'body'; const BUTTON_LOCATION_SELECTOR = '#gd5 > p:nth-child(5)';
const DATA_LIST_SELECTOR = '#gdt a'; // 修改为a标签的选择器 const DATA_LIST_SELECTOR = '#gdt a'; // 修改为a标签的选择器
const ALL_IMG_DATA = {}; // 用于储存每一页的图片url, 格式为 {"0001": "https://example001.jpg", "0002": "https://example002.jpg"}, 最高支持4位数至9999 const ALL_IMG_DATA = {}; // 用于储存每一页的图片url, 格式为 {"0001": "https://example001.jpg", "0002": "https://example002.jpg"}, 最高支持4位数至9999
@ -72,7 +72,8 @@
// 发送数据到后端的函数 // 发送数据到后端的函数
function sendDataToBackend(data) { function sendDataToBackend(data) {
console.log('准备发送的数据:', data); console.log('准备发送的数据:', data);
console.log('后端地址:', `http://${BACKEND_IP}:${BACKEND_PORT}/save_url`); console.log('数据类型:', typeof data);
console.log('字符串化后的数据:', JSON.stringify(data));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
GM_xmlhttpRequest({ GM_xmlhttpRequest({
@ -85,7 +86,6 @@
onload: function(response) { onload: function(response) {
console.log('后端响应状态:', response.status); console.log('后端响应状态:', response.status);
console.log('后端响应内容:', response.responseText); console.log('后端响应内容:', response.responseText);
console.log('响应头:', response.responseHeaders);
if (response.status === 200) { if (response.status === 200) {
resolve(response); resolve(response);
} else { } else {
@ -93,12 +93,7 @@
} }
}, },
onerror: function(error) { onerror: function(error) {
console.error('请求错误详情:', error);
reject(error); reject(error);
},
ontimeout: function() {
console.error('请求超时');
reject(new Error('请求超时'));
} }
}); });
}); });

Loading…
Cancel
Save