# -*- coding: utf-8 -*- # Copyright (c) 2025 relakkes@gmail.com # # This file is part of MediaCrawler project. # Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/api/routers/creator.py # GitHub: https://github.com/NanmiCoder # Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1 # # 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则: # 1. 不得用于任何商业用途。 # 2. 使用时应遵守目标平台的使用条款和robots.txt规则。 # 3. 不得进行大规模爬取或对平台造成运营干扰。 # 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。 # 5. 不得用于任何非法或不当的用途。 # # 详细许可条款请参阅项目根目录下的LICENSE文件。 # 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。 """运营模块的 HTTP 接口。""" import asyncio from typing import Set from fastapi import APIRouter, HTTPException, Query from ..creator import login as creator_login from ..creator import service from ..monitor.db import get_session router = APIRouter(prefix="/creator", tags=["creator"]) # 后台同步任务要留强引用:asyncio 只持弱引用,否则任务可能在跑完前被回收。 _sync_tasks: Set[asyncio.Task] = set() def _bad_request(exc: ValueError) -> HTTPException: return HTTPException(status_code=400, detail=str(exc)) @router.get("/accounts") async def list_accounts(): """账号列表。**不含 cookie**,只给 ``has_cookie``。""" async with get_session() as session: return {"accounts": await service.list_accounts(session)} @router.get("/accounts/{account_id}") async def get_account_detail(account_id: int): async with get_session() as session: try: return await service.account_detail(session, account_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) @router.delete("/accounts/{account_id}") async def delete_account(account_id: int): async with get_session() as session: try: await service.delete_account(session, account_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) return {"message": "账号已删除"} @router.post("/accounts/{account_id}/check") async def check_account(account_id: int): """重测登录态与数据权限。""" async with get_session() as session: try: return await service.check_account(session, account_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) @router.post("/accounts/{account_id}/sync") async def sync_account( account_id: int, days: int = Query(default=90, ge=1, le=730) ): """拉取作品数据。 **放后台跑**:要分页、还要按账号节流,几分钟很正常,而前端请求超时是 30 秒。 前端靠轮询账号列表里的 `last_synced_at` / `last_error` 看结果。 """ async with get_session() as session: try: await service.get_account(session, account_id) except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) task = asyncio.create_task(_sync_in_background(account_id, days)) _sync_tasks.add(task) task.add_done_callback(_sync_tasks.discard) return {"message": "同步已开始", "days": days} async def _sync_in_background(account_id: int, days: int) -> None: try: async with get_session() as session: result = await service.sync_account(session, account_id, days) print(f"[creator] 账号 {account_id} 同步完成,取回 {result['fetched']} 条") except Exception as exc: # noqa: BLE001 - 后台任务不能让异常逃逸成静默失败 print(f"[creator] 账号 {account_id} 同步失败: {exc}") # --------------------------------------------------------------------------- # 扫码新增账号 # --------------------------------------------------------------------------- # # 每次登录开一个**临时浏览器上下文**,扫完取出 cookie 就丢弃 —— 这样登第二个账号 # 不会把第一个顶掉,也不影响监控那个登录态。cookie 只在内存里从 login 模块传到 # 这里落库,**不进响应体**。 @router.post("/login") async def start_login(): try: return await creator_login.start() except RuntimeError as exc: raise HTTPException(status_code=502, detail=str(exc)) @router.get("/login") async def poll_login(): """轮询扫码结果;一旦成功就把账号落库并返回它。""" snapshot = await creator_login.status() if snapshot["status"] == creator_login.STATUS_SUCCESS and snapshot.get("account") is None: cookie = await creator_login.take_cookie() if cookie: try: async with get_session() as session: account = await service.upsert_account_from_cookie(session, cookie) except ValueError as exc: snapshot["status"] = creator_login.STATUS_ERROR snapshot["message"] = f"扫码成功但保存账号失败:{exc}" return snapshot snapshot["account"] = account snapshot["message"] = f"已添加账号:{account['nickname']}" return snapshot @router.delete("/login") async def cancel_login(): return await creator_login.cancel()