main
Jack 2 months ago
parent 68c1c143a4
commit f3942325ff
  1. 137
      index.html
  2. 231
      main.py

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画廊下载管理器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
.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; }
.header { background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 30px; text-align: center; }
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.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; }
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #1e7e34; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.gallery-list { padding: 20px; }
.gallery-item { background: white; border: 1px solid #e9ecef; border-radius: 10px; padding: 20px; margin-bottom: 15px; }
.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; }
.empty-state { text-align: center; padding: 60px 20px; color: #6c757d; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 画廊下载管理器</h1>
<p>管理您的画廊下载任务</p>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="loadGalleries()">📁 读取文件夹</button>
<button class="btn btn-success" onclick="startDownload()" id="downloadBtn"> 开始下载所有未完成</button>
<button class="btn btn-danger" onclick="cleanupJSON()" id="cleanupBtn">🗑 清理已完成JSON</button>
</div>
<div class="gallery-list" id="galleryList">
<div class="empty-state">
<h3>暂无待下载任务</h3>
<p>所有任务已完成或暂无数据</p>
</div>
</div>
</div>
<script>
async function loadGalleries() {
try {
const response = await fetch('/api/galleries');
const galleries = await response.json();
displayGalleries(galleries);
} catch (error) {
alert('读取文件夹失败: ' + error);
}
}
function displayGalleries(galleries) {
const galleryList = document.getElementById('galleryList');
if (galleries.length === 0) {
galleryList.innerHTML = '<div class="empty-state"><h3>🎉 所有任务已完成!</h3><p>没有未完成的下载任务</p></div>';
return;
}
galleryList.innerHTML = galleries.map(gallery => {
const progress = (gallery.downloaded_images / gallery.total_images) * 100;
return `
<div class="gallery-item">
<div class="gallery-title">${gallery.title}</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>
`;
}).join('');
}
async function startDownload() {
const btn = document.getElementById('downloadBtn');
btn.disabled = true;
btn.innerHTML = '⏳ 下载中...';
try {
const response = await fetch('/api/download/all', { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert('批量下载任务已开始!请查看后端控制台了解进度。');
setTimeout(loadGalleries, 5000);
} else {
alert('下载失败: ' + result.message);
}
} catch (error) {
alert('下载请求失败: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = '⬇ 开始下载所有未完成';
}
}
async function cleanupJSON() {
const btn = document.getElementById('cleanupBtn');
btn.disabled = true;
btn.innerHTML = '⏳ 清理中...';
try {
const response = await fetch('/api/cleanup', { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert(`清理完成!${result.message}`);
loadGalleries(); // 重新加载画廊列表
} else {
alert('清理失败: ' + result.message);
}
} catch (error) {
alert('清理请求失败: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = '🗑 清理已完成JSON';
}
}
document.addEventListener('DOMContentLoaded', loadGalleries);
</script>
</body>
</html>

@ -6,11 +6,12 @@ from pathlib import Path
from typing import Dict, Any, List
import asyncio
import httpx
import shutil
import aiofiles
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import uvicorn
@ -96,17 +97,25 @@ def get_all_galleries() -> List[GalleryInfo]:
downloaded_count = 0
if 'all_images' in data:
for filename, url in data['all_images'].items():
image_path = gallery_dir / filename
if image_path.exists():
# 获取目录下所有图片文件
image_files = list(gallery_dir.glob("*.*"))
image_filenames = {file.stem for file in image_files if file.is_file() and file.name != "data.json"}
# 检查JSON中每个图片是否有对应的实际文件(忽略后缀名)
for filename in data['all_images'].keys():
# 移除可能的扩展名(如果有的话),只比较文件名主体
filename_stem = Path(filename).stem
if filename_stem in image_filenames:
downloaded_count += 1
galleries.append(GalleryInfo(
title=data.get('title', gallery_dir.name),
path=str(gallery_dir),
total_images=data.get('total_images', 0),
downloaded_images=downloaded_count
))
# 只显示未完成的任务(下载进度不是100%的)
if downloaded_count < data.get('total_images', 0):
galleries.append(GalleryInfo(
title=data.get('title', gallery_dir.name),
path=str(gallery_dir),
total_images=data.get('total_images', 0),
downloaded_images=downloaded_count
))
except Exception as e:
logger.error(f"读取画廊数据失败 {gallery_dir}: {e}")
@ -130,7 +139,8 @@ async def download_single_image(client: httpx.AsyncClient, url: str, file_path:
# 创建带后缀的文件路径
file_path_with_suffix = file_path.with_suffix('.' + suffix)
if file_path_with_suffix.exists():
# 检查是否已存在(考虑所有可能的扩展名)
if check_image_exists(file_path):
return True
img_response = await client.get(real_img_url, timeout=DOWNLOAD_TIMEOUT)
@ -145,6 +155,25 @@ async def download_single_image(client: httpx.AsyncClient, url: str, file_path:
logger.error(f"下载失败 {url}: {e}")
return False
def check_image_exists(file_path: Path) -> bool:
"""检查图片文件是否存在(忽略扩展名)"""
if file_path.exists():
return True
# 检查是否有相同文件名但不同扩展名的文件
parent_dir = file_path.parent
stem = file_path.stem
# 常见的图片扩展名
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
for ext in image_extensions:
potential_file = parent_dir / f"{stem}{ext}"
if potential_file.exists():
return True
return False
async def download_gallery_images(title: str) -> DownloadStatusResponse:
safe_title = sanitize_filename(title)
gallery_path = downloads_path / safe_title
@ -197,7 +226,8 @@ async def download_gallery_images(title: str) -> DownloadStatusResponse:
for filename, url in all_images.items():
image_path = gallery_path / filename
if image_path.exists():
# 使用新的检查方法,忽略扩展名
if check_image_exists(image_path):
download_status[title]["downloaded"] += 1
continue
@ -264,6 +294,46 @@ async def download_all_pending_galleries():
logger.info("批量下载任务完成")
def delete_completed_json_files():
"""删除已完成任务的JSON文件"""
downloads_path = Path(DOWNLOADS_DIR)
deleted_count = 0
if not downloads_path.exists():
return deleted_count
for gallery_dir in downloads_path.iterdir():
if gallery_dir.is_dir():
data_file = gallery_dir / "data.json"
if data_file.exists():
try:
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 检查是否所有图片都已下载
downloaded_count = 0
if 'all_images' in data:
image_files = list(gallery_dir.glob("*.*"))
image_filenames = {file.stem for file in image_files if file.is_file() and file.name != "data.json"}
for filename in data['all_images'].keys():
filename_stem = Path(filename).stem
if filename_stem in image_filenames:
downloaded_count += 1
total_images = len(data.get('all_images', {}))
# 如果所有图片都已下载,删除JSON文件
if downloaded_count == total_images and total_images > 0:
data_file.unlink()
deleted_count += 1
logger.info(f"已删除已完成任务的JSON文件: {gallery_dir.name}")
except Exception as e:
logger.error(f"处理画廊目录失败 {gallery_dir}: {e}")
return deleted_count
# 初始化
downloads_path = setup_downloads_directory()
@ -295,128 +365,9 @@ async def save_url_data(request: SaveDataRequest = None):
logger.error(f"保存数据失败: {e}")
raise HTTPException(status_code=500, detail=f"保存失败: {str(e)}")
@app.get("/", response_class=HTMLResponse)
@app.get("/")
async def read_gallery_manager():
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>画廊下载管理器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }
.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; }
.header { background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 30px; text-align: center; }
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.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; }
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #1e7e34; }
.gallery-list { padding: 20px; }
.gallery-item { background: white; border: 1px solid #e9ecef; border-radius: 10px; padding: 20px; margin-bottom: 15px; }
.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; }
.empty-state { text-align: center; padding: 60px 20px; color: #6c757d; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 画廊下载管理器</h1>
<p>管理您的画廊下载任务</p>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="loadGalleries()">📁 读取文件夹</button>
<button class="btn btn-success" onclick="startDownload()" id="downloadBtn"> 开始下载所有未完成</button>
</div>
<div class="gallery-list" id="galleryList">
<div class="empty-state">
<h3>暂无待下载任务</h3>
<p>点击"读取文件夹"按钮加载数据</p>
</div>
</div>
</div>
<script>
async function loadGalleries() {
try {
const response = await fetch('/api/galleries');
const galleries = await response.json();
displayGalleries(galleries);
} catch (error) {
alert('读取文件夹失败: ' + error);
}
}
function displayGalleries(galleries) {
const galleryList = document.getElementById('galleryList');
if (galleries.length === 0) {
galleryList.innerHTML = '<div class="empty-state"><h3>暂无画廊数据</h3></div>';
return;
}
const pendingGalleries = galleries.filter(gallery => gallery.downloaded_images < gallery.total_images);
if (pendingGalleries.length === 0) {
galleryList.innerHTML = '<div class="empty-state"><h3>🎉 所有任务已完成!</h3></div>';
return;
}
galleryList.innerHTML = pendingGalleries.map(gallery => {
const progress = (gallery.downloaded_images / gallery.total_images) * 100;
return `
<div class="gallery-item">
<div class="gallery-title">${gallery.title}</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>
`;
}).join('');
}
async function startDownload() {
const btn = document.getElementById('downloadBtn');
btn.disabled = true;
btn.innerHTML = '⏳ 下载中...';
try {
const response = await fetch('/api/download/all', { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert('批量下载任务已开始!请查看后端控制台了解进度。');
setTimeout(loadGalleries, 5000);
} else {
alert('下载失败: ' + result.message);
}
} catch (error) {
alert('下载请求失败: ' + error);
} finally {
btn.disabled = false;
btn.innerHTML = ' 开始下载所有未完成';
}
}
document.addEventListener('DOMContentLoaded', loadGalleries);
</script>
</body>
</html>
"""
return FileResponse("index.html")
@app.get("/api/galleries")
async def get_galleries():
@ -440,6 +391,20 @@ async def download_gallery(title: str, background_tasks: BackgroundTasks):
"title": title
}
@app.post("/api/cleanup")
async def cleanup_completed_galleries():
"""清理已完成任务的JSON文件"""
try:
deleted_count = delete_completed_json_files()
return {
"status": "success",
"message": f"成功删除 {deleted_count} 个已完成任务的JSON文件",
"deleted_count": deleted_count
}
except Exception as e:
logger.error(f"清理JSON文件失败: {e}")
raise HTTPException(status_code=500, detail=f"清理失败: {str(e)}")
@app.get("/health")
async def health_check():
return {"status": "healthy"}

Loading…
Cancel
Save