|
|
|
|
@ -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,11 +97,19 @@ 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 |
|
|
|
|
|
|
|
|
|
# 只显示未完成的任务(下载进度不是100%的) |
|
|
|
|
if downloaded_count < data.get('total_images', 0): |
|
|
|
|
galleries.append(GalleryInfo( |
|
|
|
|
title=data.get('title', gallery_dir.name), |
|
|
|
|
path=str(gallery_dir), |
|
|
|
|
@ -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"} |
|
|
|
|
|