jack 3 bulan lalu
induk
melakukan
a0562eb173
10 mengubah file dengan 202 tambahan dan 27 penghapusan
  1. 0 1
      data/targets.txt
  2. 0 6
      failed_downloads.json
  3. 72 4
      main.py
  4. TEMPAT SAMPAH
      static/favicon.ico
  5. 66 0
      static/script.js
  6. 20 0
      static/style.css
  7. 10 7
      step1.py
  8. 8 4
      step2.py
  9. 26 3
      templates/index.html
  10. 0 2
      utils.py

+ 0 - 1
data/targets.txt

@@ -1 +0,0 @@
-https://e-hentai.org/g/3550066/47d6393550

+ 0 - 6
failed_downloads.json

@@ -1,6 +0,0 @@
-[
-  {
-    "img_path": "data/downloads/[Pixiv]玲殿下(81002566)2025.09.23-E-HentaiGalleries/0016",
-    "img_url": "https://e-hentai.org/s/e3f2a8c9a8/3550066-16"
-  }
-]

+ 72 - 4
main.py

@@ -1,13 +1,16 @@
-from fastapi import FastAPI, Request, Form
+from fastapi import FastAPI, Request
 from fastapi.staticfiles import StaticFiles
+from fastapi.responses import FileResponse
 from fastapi.templating import Jinja2Templates
 from fastapi.responses import JSONResponse
 import uvicorn
+import glob
 import os
 from pydantic import BaseModel
+import step2
 from utils import *
 
-app = FastAPI(title="下载工具", version="1.0.0")
+app = FastAPI(title="EH-Downloader", version="1.0.0")
 
 # 在应用启动时检查并创建data文件夹和targets.txt
 @app.on_event("startup")
@@ -24,8 +27,7 @@ async def startup_event():
         with open(targets_file, 'w', encoding='utf-8') as f:
             f.write("# 在这里添加目标URL,每行一个\n")
             f.write("# 示例:\n")
-            f.write("# https://example.com/file1.zip\n")
-            f.write("# https://example.com/image.jpg\n")
+            f.write("https://e-hentai.org/g/3550066/47d6393550\n")
         print(f"创建文件: {targets_file}")
     else:
         print(f"文件已存在: {targets_file}")
@@ -34,6 +36,11 @@ async def startup_event():
 app.mount("/static", StaticFiles(directory="static"), name="static")
 templates = Jinja2Templates(directory="templates")
 
+# favicon 路由
+@app.get("/favicon.ico", include_in_schema=False)
+async def favicon():
+    return FileResponse("static/favicon.ico")
+
 @app.get("/")
 async def home(request: Request):
     """主页面"""
@@ -97,5 +104,66 @@ async def download_images(req: ProxyRequest):
     msg = await run_step2(proxy)
     return JSONResponse({"success": True, "message": msg})
 
+@app.post("/clean_files")
+async def clean_files():
+    """清理项目目录下的所有 .log 和 .json 文件"""
+    try:
+        deleted_files = []
+        error_files = []
+        
+        # 查找当前目录及所有子目录中的 .log 和 .json 文件
+        patterns = ["**/*.log", "**/*.json"]
+        
+        for pattern in patterns:
+            for file_path in glob.glob(pattern, recursive=True):
+                try:
+                    # 跳过 data/targets.txt 文件,因为这是配置文件
+                    if file_path == "data/targets.txt":
+                        continue
+                    
+                    os.remove(file_path)
+                    deleted_files.append(file_path)
+                    print(f"已删除文件: {file_path}")
+                except Exception as e:
+                    error_files.append(f"{file_path}: {str(e)}")
+                    print(f"删除文件失败 {file_path}: {str(e)}")
+        
+        if error_files:
+            return JSONResponse({
+                "success": False,
+                "message": f"清理完成,但部分文件删除失败",
+                "deleted_count": len(deleted_files),
+                "error_count": len(error_files),
+                "deleted_files": deleted_files,
+                "error_files": error_files
+            })
+        else:
+            return JSONResponse({
+                "success": True,
+                "message": f"成功清理 {len(deleted_files)} 个文件",
+                "deleted_count": len(deleted_files),
+                "error_count": 0,
+                "deleted_files": deleted_files
+            })
+            
+    except Exception as e:
+        return JSONResponse({
+            "success": False,
+            "message": f"清理过程中出错: {str(e)}",
+            "deleted_count": 0,
+            "error_count": 0
+        })
+
+@app.post("/check_incomplete")
+async def check_incomplete():
+    result = await step2.scan_tasks()
+
+    """检查未完成文件"""
+    return JSONResponse({
+        "success": True,
+        "message": "检查未完成文件功能已就绪",
+        "data": f"共 {len(result)} 个文件未下载"
+    })
+
 if __name__ == "__main__":
     uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

TEMPAT SAMPAH
static/favicon.ico


+ 66 - 0
static/script.js

@@ -5,7 +5,9 @@ class DownloadTool {
         this.loadUrlsBtn = document.getElementById('loadUrls');
         this.urlListTextarea = document.getElementById('urlList');
         this.downloadUrlBtn = document.getElementById('downloadUrl');
+        this.cleanFilesBtn = document.getElementById('cleanFiles');
         this.downloadImageBtn = document.getElementById('downloadImage');
+        this.checkIncompleteBtn = document.getElementById('checkIncomplete');
         this.clearOutputBtn = document.getElementById('clearOutput');
         
         this.initEvents();
@@ -26,6 +28,16 @@ class DownloadTool {
         this.downloadImageBtn.addEventListener('click', () => {
             this.downloadImages()
         });
+
+        // 检查未完成按钮
+        this.checkIncompleteBtn.addEventListener('click', () => {
+            this.checkIncomplete();
+        });
+
+        // 清理文件按钮
+        this.cleanFilesBtn.addEventListener('click', () => {
+            this.cleanFiles();
+        });
         
         // 清除输出按钮
         this.clearOutputBtn.addEventListener('click', () => {
@@ -99,6 +111,60 @@ class DownloadTool {
         this.showOutput(data.message, data.success ? 'success' : 'error');
     }
 
+    async checkIncomplete() {
+        try {
+            this.showOutput('正在检查未完成文件...', 'info');
+            
+            const response = await fetch('/check_incomplete', {
+                method: 'POST'
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                // 这里先显示后端返回的测试数据,等您完成后端逻辑后会返回实际数据
+                let message = `检查完成!\n\n`;
+                message += `返回数据: ${JSON.stringify(result.data, null, 2)}`;
+                this.showOutput(message, 'success');
+            } else {
+                this.showOutput(`检查失败: ${result.message}`, 'error');
+            }
+        } catch (error) {
+            this.showOutput(`检查未完成文件时出错: ${error.message}`, 'error');
+        }
+    }
+
+    async cleanFiles() {
+        try {
+            this.showOutput('正在清理日志和JSON文件...', 'info');
+            
+            const response = await fetch('/clean_files', {
+                method: 'POST'
+            });
+            
+            const result = await response.json();
+            
+            if (result.success) {
+                let message = `清理完成!成功删除 ${result.deleted_count} 个文件\n\n`;
+                if (result.deleted_files && result.deleted_files.length > 0) {
+                    message += "已删除的文件:\n" + result.deleted_files.join('\n');
+                }
+                this.showOutput(message, 'success');
+            } else {
+                let message = `清理完成,但有 ${result.error_count} 个文件删除失败\n\n`;
+                if (result.deleted_files && result.deleted_files.length > 0) {
+                    message += "已删除的文件:\n" + result.deleted_files.join('\n') + '\n\n';
+                }
+                if (result.error_files && result.error_files.length > 0) {
+                    message += "删除失败的文件:\n" + result.error_files.join('\n');
+                }
+                this.showOutput(message, 'error');
+            }
+        } catch (error) {
+            this.showOutput(`清理文件时出错: ${error.message}`, 'error');
+        }
+    }
+
     showOutput(message, type = '') {
         this.output.textContent = message;
         this.output.className = 'output-area';

+ 20 - 0
static/style.css

@@ -130,6 +130,26 @@ input[type="url"]:focus {
     transform: translateY(-2px);
 }
 
+.btn-warning {
+    background: #ffc107;
+    color: #212529;
+}
+
+.btn-warning:hover {
+    background: #e0a800;
+    transform: translateY(-2px);
+}
+
+.btn-success {
+    background: #28a745;
+    color: white;
+}
+
+.btn-success:hover {
+    background: #218838;
+    transform: translateY(-2px);
+}
+
 .output-section h3 {
     color: #333;
     margin-bottom: 10px;

+ 10 - 7
step1.py

@@ -9,12 +9,12 @@ from __future__ import annotations
 import asyncio
 import json
 import logging
+import os
 import re
 import sys
 from pathlib import Path
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, Optional
 
-import aiofiles
 import httpx
 from bs4 import BeautifulSoup
 from tqdm.asyncio import tqdm_asyncio
@@ -26,19 +26,22 @@ MAX_PAGE = 100                   # 单专辑最大翻页
 RETRY_PER_PAGE = 5               # 单页重试
 TIMEOUT = httpx.Timeout(10.0)    # 请求超时
 IMG_SELECTOR = "#gdt"            # 图片入口区域
-FAILED_RECORD = "failed_keys.json"
+FAILED_RECORD = "data/failed_keys.json"
 LOG_LEVEL = logging.INFO
 # ----------------------------------------------------
 
+if not os.path.exists("data"):
+    os.mkdir("data")
+
 logging.basicConfig(
     level=LOG_LEVEL,
     format="[%(asctime)s] [%(levelname)s] %(message)s",
     handlers=[
         logging.StreamHandler(sys.stdout),
-        logging.FileHandler("crawl.log", encoding="utf-8"),
+        logging.FileHandler("data/crawl.log", encoding="utf-8"),
     ],
 )
-log = logging.getLogger("eh_crawler")
+log = logging.getLogger("data/eh_crawler")
 
 # 预编译正则
 ILLEGAL_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1F]')
@@ -56,11 +59,11 @@ def load_targets() -> List[str]:
     if not tgt.exists():
         log.error("targets.txt 不存在,已自动创建,请先填写 URL")
         tgt.touch()
-        sys.exit(0)
+        return
     lines = [ln.strip() for ln in tgt.read_text(encoding="utf-8").splitlines() if ln.strip()]
     if not lines:
         log.error("targets.txt 为空,请先填写 URL")
-        sys.exit(0)
+        return
     return list(set(lines))  # 去重
 
 

+ 8 - 4
step2.py

@@ -9,10 +9,11 @@ from __future__ import annotations
 import asyncio
 import json
 import logging
+import os
 import re
 import sys
 from pathlib import Path
-from typing import Dict, List, Optional
+from typing import Dict, List
 
 import aiofiles
 import httpx
@@ -23,19 +24,22 @@ from tqdm.asyncio import tqdm_asyncio
 CONCURRENCY = 20                 # 并发下载数
 RETRY_PER_IMG = 3                # 单图重试
 TIMEOUT = httpx.Timeout(15.0)    # 请求超时
-FAILED_RECORD = "failed_downloads.json"
+FAILED_RECORD = "data/failed_downloads.json"
 LOG_LEVEL = logging.INFO
 # ----------------------------------------------------
 
+if not os.path.exists("data"):
+    os.mkdir("data")
+
 logging.basicConfig(
     level=LOG_LEVEL,
     format="[%(asctime)s] [%(levelname)s] %(message)s",
     handlers=[
         logging.StreamHandler(sys.stdout),
-        logging.FileHandler("download.log", encoding="utf-8"),
+        logging.FileHandler("data/download.log", encoding="utf-8"),
     ],
 )
-log = logging.getLogger("img_downloader")
+log = logging.getLogger("data/img_downloader")
 
 # 预编译正则
 IMG_URL_RE = re.compile(r'<img id="img" src="(.*?)"', re.S)

+ 26 - 3
templates/index.html

@@ -3,12 +3,12 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>下载工具</title>
+    <title>EH-Downloader</title>
     <link rel="stylesheet" href="/static/style.css">
 </head>
 <body>
     <div class="container">
-        <h1>下载工具</h1>
+        <h1>EH</h1>
         
         <form id="downloadForm" class="form">
             <div class="form-row">
@@ -35,14 +35,37 @@
             <div class="button-group">
                 <button type="button" id="downloadUrl" class="btn btn-primary">下载URL</button>
                 <button type="button" id="downloadImage" class="btn btn-secondary">下载图片</button>
+                <button type="button" id="checkIncomplete" class="btn btn-info">检查未完成</button>
+                <button type="button" id="cleanFiles" class="btn btn-warning">清理日志和JSON文件</button>
                 <button type="button" id="clearOutput" class="btn btn-danger">清除输出</button>
             </div>
         </form>
         
         <div class="output-section">
-            <h3>输出将显示在这里...</h3>
+            <h3>以下是一个输出框, 但貌似没啥卵用...</h3>
             <pre id="output" class="output-area"></pre>
         </div>
+
+        <br><br>
+
+        <div class="output-section">
+            <h3>使用说明</h3>
+            <div class="usage-instructions">
+                <p><strong>工具使用步骤:</strong></p>
+                <ol>
+                    <li>设置代理IP和端口(如使用魔法工具)</li>
+                    <li>在<a href="https://e-hentai.org/" target="_blank">点解这里</a>获取需要下载的画廊URL</li>
+                    <li>将URL复制到项目目录下的data/targets.txt中, 一个画廊一个URL</li>
+                    <li><del>不要问问什么不直接填到页面, 我懒得写</del></li>
+                    <li>点击"读取目标URL"加载 targets.txt 中的URL列表</li>
+                    <li>点击"下载URL"开始抓取画廊链接</li>
+                    <li>点击"下载图片"开始下载图片文件</li>
+                    <li>使用"检查未完成"查看下载进度</li>
+                    <li>使用"清理日志和JSON文件"清理临时文件</li>
+                </ol>
+                <p><strong>注意:</strong>请确保代理设置正确,且 targets.txt 文件中已添加目标URL。</p>
+            </div>
+        </div>
     </div>
     
     <script src="/static/script.js"></script>

+ 0 - 2
utils.py

@@ -1,9 +1,7 @@
 # utils.py
-import asyncio
 from pathlib import Path
 from typing import List
 
-from aiopath import AsyncPath
 import logging
 
 # 把 1step.py 的主逻辑封装成函数