parent
875ae133c0
commit
a16c47b303
@ -0,0 +1,73 @@ |
|||||||
|
from fastapi import APIRouter, BackgroundTasks |
||||||
|
from pydantic import BaseModel |
||||||
|
import uuid |
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1", tags=["downloader"]) |
||||||
|
|
||||||
|
# 存储任务状态 |
||||||
|
tasks = {} |
||||||
|
|
||||||
|
class CrawlRequest(BaseModel): |
||||||
|
url: str |
||||||
|
cookies: str |
||||||
|
timestamp: str |
||||||
|
|
||||||
|
class TaskStatus(BaseModel): |
||||||
|
status: str # 'running', 'completed', 'failed' |
||||||
|
result: dict = None |
||||||
|
error: str = None |
||||||
|
|
||||||
|
@router.post("/start-crawl") |
||||||
|
async def start_crawl(request: CrawlRequest, background_tasks: BackgroundTasks): |
||||||
|
task_id = str(uuid.uuid4()) |
||||||
|
tasks[task_id] = {'status': 'running', 'result': None, 'error': None} |
||||||
|
|
||||||
|
# 在后台运行爬虫任务 |
||||||
|
background_tasks.add_task(run_crawler, task_id, request) |
||||||
|
|
||||||
|
return {"task_id": task_id, "status": "started"} |
||||||
|
|
||||||
|
@router.get("/task-status/{task_id}") |
||||||
|
async def get_task_status(task_id: str): |
||||||
|
task = tasks.get(task_id) |
||||||
|
if not task: |
||||||
|
return {"status": "not_found"} |
||||||
|
return task |
||||||
|
|
||||||
|
async def run_crawler(task_id: str, request: CrawlRequest): |
||||||
|
try: |
||||||
|
# 这里执行您的爬虫逻辑,模拟长时间运行 |
||||||
|
# 例如:time.sleep(300) # 5分钟 |
||||||
|
|
||||||
|
# 确保 downloads 目录存在(双重保障) |
||||||
|
downloads_dir = Path("downloads") |
||||||
|
downloads_dir.mkdir(exist_ok=True) |
||||||
|
|
||||||
|
# 模拟下载文件到 downloads 目录 |
||||||
|
filename = f"download_{task_id}.txt" |
||||||
|
filepath = downloads_dir / filename |
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f: |
||||||
|
f.write(f"URL: {request.url}\n") |
||||||
|
f.write(f"Cookies: {request.cookies}\n") |
||||||
|
f.write(f"Timestamp: {request.timestamp}\n") |
||||||
|
f.write("Download completed successfully\n") |
||||||
|
|
||||||
|
# 爬虫完成后更新状态 |
||||||
|
tasks[task_id] = { |
||||||
|
'status': 'completed', |
||||||
|
'result': { |
||||||
|
'message': '爬虫完成', |
||||||
|
'data': '您的爬虫结果', |
||||||
|
'download_path': str(filepath) |
||||||
|
}, |
||||||
|
'error': None |
||||||
|
} |
||||||
|
except Exception as e: |
||||||
|
tasks[task_id] = { |
||||||
|
'status': 'failed', |
||||||
|
'result': None, |
||||||
|
'error': str(e) |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
from fastapi import FastAPI |
||||||
|
from contextlib import asynccontextmanager |
||||||
|
import uvicorn |
||||||
|
from pathlib import Path |
||||||
|
from downloader import router as downloader_router |
||||||
|
|
||||||
|
# 检查并创建 downloads 目录 |
||||||
|
def ensure_downloads_dir(): |
||||||
|
downloads_dir = Path("downloads") |
||||||
|
downloads_dir.mkdir(exist_ok=True) |
||||||
|
print(f"确保 downloads 目录存在: {downloads_dir.absolute()}") |
||||||
|
|
||||||
|
# lifespan 事件处理器 |
||||||
|
@asynccontextmanager |
||||||
|
async def lifespan(app: FastAPI): |
||||||
|
# 启动时执行 |
||||||
|
ensure_downloads_dir() |
||||||
|
print("应用启动完成!") |
||||||
|
yield |
||||||
|
# 关闭时执行(可选) |
||||||
|
print("应用正在关闭...") |
||||||
|
|
||||||
|
app = FastAPI( |
||||||
|
title="下载器API", |
||||||
|
description="一个基于FastAPI的异步下载器服务", |
||||||
|
version="1.0.0", |
||||||
|
lifespan=lifespan |
||||||
|
) |
||||||
|
|
||||||
|
# 注册路由 |
||||||
|
app.include_router(downloader_router) |
||||||
|
|
||||||
|
@app.get("/") |
||||||
|
async def root(): |
||||||
|
return {"message": "下载器服务运行中", "status": "healthy"} |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
uvicorn.run( |
||||||
|
"main:app", |
||||||
|
host="0.0.0.0", |
||||||
|
port=5100, |
||||||
|
reload=True # 开发时自动重载 |
||||||
|
) |
||||||
@ -0,0 +1,200 @@ |
|||||||
|
// ==UserScript==
|
||||||
|
// @name 数据发送工具
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description 向本地后端发送当前页面的URL和Cookies
|
||||||
|
// @author You
|
||||||
|
// @match *://*/*
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @connect 127.0.0.1
|
||||||
|
// @connect localhost
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() { |
||||||
|
'use strict'; |
||||||
|
|
||||||
|
// 配置:您可以修改这些变量来自定义行为
|
||||||
|
const TARGET_SELECTOR = 'body'; // 按钮插入位置的选择器
|
||||||
|
const BACKEND_IP = '127.0.0.1'; // 后端IP地址
|
||||||
|
const BACKEND_PORT = '5100'; // 后端端口号
|
||||||
|
|
||||||
|
// 构建后端基础URL
|
||||||
|
const BACKEND_BASE_URL = `http://${BACKEND_IP}:${BACKEND_PORT}`; |
||||||
|
|
||||||
|
function addButton() { |
||||||
|
if (document.getElementById('data-sender-button')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const button = document.createElement('button'); |
||||||
|
button.id = 'data-sender-button'; |
||||||
|
button.textContent = "send data"; |
||||||
|
button.style.position = "fixed"; |
||||||
|
button.style.top = "12.5%"; |
||||||
|
button.style.right = "1%"; |
||||||
|
button.style.transform = "translateY(-50%)"; |
||||||
|
button.style.padding = "3px 8px"; |
||||||
|
button.style.fontSize = "10px"; |
||||||
|
button.style.backgroundColor = "#007baf"; |
||||||
|
button.style.color = "#fff"; |
||||||
|
button.style.border = "none"; |
||||||
|
button.style.borderRadius = "5px"; |
||||||
|
button.style.cursor = "pointer"; |
||||||
|
button.style.zIndex = "10000"; |
||||||
|
|
||||||
|
button.addEventListener('click', function() { |
||||||
|
sendDataToBackend(); |
||||||
|
}); |
||||||
|
|
||||||
|
const targetElement = document.querySelector(TARGET_SELECTOR); |
||||||
|
|
||||||
|
if (targetElement && TARGET_SELECTOR !== 'body') { |
||||||
|
const buttonContainer = document.createElement('div'); |
||||||
|
buttonContainer.style.display = 'inline-block'; |
||||||
|
buttonContainer.style.marginLeft = '10px'; |
||||||
|
|
||||||
|
button.style.position = 'relative'; |
||||||
|
button.style.top = 'auto'; |
||||||
|
button.style.right = 'auto'; |
||||||
|
button.style.transform = 'none'; |
||||||
|
button.style.margin = '0'; |
||||||
|
|
||||||
|
buttonContainer.appendChild(button); |
||||||
|
|
||||||
|
if (targetElement.nextSibling) { |
||||||
|
targetElement.parentNode.insertBefore(buttonContainer, targetElement.nextSibling); |
||||||
|
} else { |
||||||
|
targetElement.parentNode.appendChild(buttonContainer); |
||||||
|
} |
||||||
|
} else { |
||||||
|
document.body.appendChild(button); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function sendDataToBackend() { |
||||||
|
const currentUrl = window.location.href; |
||||||
|
const cookies = document.cookie; |
||||||
|
|
||||||
|
const data = { |
||||||
|
url: currentUrl, |
||||||
|
cookies: cookies, |
||||||
|
timestamp: new Date().toISOString() |
||||||
|
}; |
||||||
|
|
||||||
|
// 禁用按钮防止重复点击
|
||||||
|
const button = document.getElementById('data-sender-button'); |
||||||
|
if (button) { |
||||||
|
button.disabled = true; |
||||||
|
button.textContent = "任务进行中..."; |
||||||
|
button.style.backgroundColor = "#6c757d"; |
||||||
|
} |
||||||
|
|
||||||
|
// 发送任务请求
|
||||||
|
GM_xmlhttpRequest({ |
||||||
|
method: "POST", |
||||||
|
url: `${BACKEND_BASE_URL}/start-crawl`, |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json" |
||||||
|
}, |
||||||
|
data: JSON.stringify(data), |
||||||
|
onload: function(response) { |
||||||
|
if (response.status === 200) { |
||||||
|
const result = JSON.parse(response.responseText); |
||||||
|
if (result.task_id) { |
||||||
|
alert("爬虫任务已启动!任务ID: " + result.task_id); |
||||||
|
// 开始轮询任务状态
|
||||||
|
pollTaskStatus(result.task_id); |
||||||
|
} else { |
||||||
|
alert("任务启动失败: " + (result.message || "未知错误")); |
||||||
|
resetButton(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
alert("请求失败,状态码: " + response.status); |
||||||
|
resetButton(); |
||||||
|
} |
||||||
|
}, |
||||||
|
onerror: function(error) { |
||||||
|
console.error("数据发送失败:", error); |
||||||
|
alert("数据发送失败,请检查后端服务是否运行"); |
||||||
|
resetButton(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function pollTaskStatus(taskId) { |
||||||
|
let pollCount = 0; |
||||||
|
const maxPolls = 300; // 最多轮询300次(5分钟,每秒一次)
|
||||||
|
|
||||||
|
const pollInterval = setInterval(() => { |
||||||
|
pollCount++; |
||||||
|
|
||||||
|
GM_xmlhttpRequest({ |
||||||
|
method: "GET", |
||||||
|
url: `${BACKEND_BASE_URL}/task-status/${taskId}`, |
||||||
|
onload: function(response) { |
||||||
|
if (response.status === 200) { |
||||||
|
const result = JSON.parse(response.responseText); |
||||||
|
|
||||||
|
// 更新按钮状态显示进度
|
||||||
|
const button = document.getElementById('data-sender-button'); |
||||||
|
if (button) { |
||||||
|
button.textContent = `任务中...${pollCount}s`; |
||||||
|
} |
||||||
|
|
||||||
|
if (result.status === 'completed') { |
||||||
|
clearInterval(pollInterval); |
||||||
|
alert("爬虫任务完成!\n结果: " + JSON.stringify(result.result, null, 2)); |
||||||
|
resetButton(); |
||||||
|
} else if (result.status === 'failed') { |
||||||
|
clearInterval(pollInterval); |
||||||
|
alert("爬虫任务失败: " + result.error); |
||||||
|
resetButton(); |
||||||
|
} |
||||||
|
// 如果状态是 'running',继续轮询
|
||||||
|
} else { |
||||||
|
console.error("获取任务状态失败:", response.status); |
||||||
|
} |
||||||
|
}, |
||||||
|
onerror: function(error) { |
||||||
|
console.error("轮询任务状态失败:", error); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 超过最大轮询次数,停止轮询
|
||||||
|
if (pollCount >= maxPolls) { |
||||||
|
clearInterval(pollInterval); |
||||||
|
alert("任务超时,请稍后手动检查结果"); |
||||||
|
resetButton(); |
||||||
|
} |
||||||
|
}, 1000); // 每秒轮询一次
|
||||||
|
} |
||||||
|
|
||||||
|
function resetButton() { |
||||||
|
const button = document.getElementById('data-sender-button'); |
||||||
|
if (button) { |
||||||
|
button.disabled = false; |
||||||
|
button.textContent = "send data"; |
||||||
|
button.style.backgroundColor = "#007baf"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 初始尝试添加按钮
|
||||||
|
addButton(); |
||||||
|
|
||||||
|
// 使用MutationObserver监听DOM变化
|
||||||
|
const observer = new MutationObserver(function(mutations) { |
||||||
|
addButton(); |
||||||
|
}); |
||||||
|
|
||||||
|
observer.observe(document.body, { |
||||||
|
childList: true, |
||||||
|
subtree: true |
||||||
|
}); |
||||||
|
|
||||||
|
if (document.readyState === 'loading') { |
||||||
|
document.addEventListener('DOMContentLoaded', addButton); |
||||||
|
} else { |
||||||
|
addButton(); |
||||||
|
} |
||||||
|
|
||||||
|
})(); |
||||||
Loading…
Reference in new issue