|
|
|
@ -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__": |
|
|
|
|