This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/web/routers/delivery.py
T
snowy-yang 6288a98ecc refactor: 拆分 4 个巨石模块为包,清理过时注释并删除作废文档
为什么:单文件 1100~1900 行的巨石模块让「改一处要通读全文」;同时注释里残留
大量已删台账的编号,以及「Flask/FastAPI 双栈迁移期」这类已失实的架构描述,
会误导后来者按不复存在的架构去改代码。

拆分(对外导入路径不变,行为零变更):
- music_downloader.py  -> music_downloader/(models/downloader/cover/_http/_log/_concurrency)
- auth_system.py       -> auth_system/(db/security/users/credits/ledger/mail/cards/backup...)
- music_api.py         -> music_api/(api/client/crypto/qr/cookies/compat/constants/errors)
- services/runtime.py  -> services/(api_service/_shared/_taskcore/downloads/batch/podcast/
  streaming/tasks);services/runtime.py 保留为兼容门面

兼容性做法:各包用 __init__.py 以 __all__ 原样再导出全部符号,既有调用点
(auth_system.xxx / services.xxx / from music_api import Y)零改动;路径常量改为经包属性
动态读取(auth_system.DB_PATH),否则测试的 monkeypatch 会静默失效并写到生产库。

修复 9 处代码损伤(上一轮注释清理把 `()` 当「空括号残留」删掉所致;用「与拆分前逐字
等价」校验逐一定位,静态检查只会报出其中 4 处):
- core/config.py  ).strip() or '*'
- core/state.py   threading.Lock() x2
- qr_login.py     datetime.now() / .strip()
- music_downloader/downloader.py  .lower()
- services/podcast.py  set() / time.time() x2

清理与删除:
- 删除 部署文档.md / 功能介绍.md / README_私有包警告.txt
- 清理指向已删台账与文档的过时编号注释;把「迁移期 / 两栈 / 双框架 / 已迁移」等
  失实描述改写为现状描述(保留 `<-> Flask /path` 映射表等仍有价值的契约来源说明)
- _mkpkg.py:公开包剔除清单去掉已不存在的文档名;私有包警告改为自包含
- pyproject.toml:补 pythonpath = ["."],使裸跑 pytest 不再 import core 失败

验证:ruff 全通过;pyright 0 errors / 0 warnings(102 文件);pytest 144 passed;
真实 uvicorn 冒烟 6 条路由符合预期且生产 data/ 指纹未变;忽略注释的逐字等价性校验通过。
2026-09-26 12:37:48 +08:00

112 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""结果交付端点。
端点:
- POST/GET /api/v1/download/result ↔ Flask /download/result(单曲)
- POST/GET /api/v1/download/batch_zip/result ↔ Flask /download/batch_zip/result(批量 ZIP)
语义对齐要点:
- 任务归属:仅任务本人的站点账号可领取(非本人 403)
- 状态语义:进行中 202 / 暂停 200+提示 / 取消 200+原因 / 失败 500+原因 /
文件缺失 404(与历史分支逐一对应)
- 交付物:FileResponse 流式发送;Cache-Control: no-store;
X-Download-Filename(URL 编码,供前端取回原始文件名)
- 文件清理仍由监控线程按 35 分钟保留期统一回收(交付请求不删文件)
"""
import asyncio
import os
from typing import Annotated, Any
from urllib.parse import quote
from fastapi import APIRouter, Depends, Request
from fastapi.responses import FileResponse
from core.state import DOWNLOAD_TASKS, DOWNLOAD_TASKS_LOCK
from web.deps import SiteUser, require_login
from web.request_utils import (
request_data as _request_data,
)
from web.response import error
router = APIRouter()
LoginUser = Annotated[SiteUser, Depends(require_login)]
def _owned_by(task: dict[str, Any], user: SiteUser) -> bool:
return int(task.get('user_id') or 0) == int(user.id)
def _file_response(target: str, filename: str, mimetype: str) -> FileResponse:
resp = FileResponse(target, media_type=mimetype, filename=filename)
resp.headers['Cache-Control'] = 'no-store'
resp.headers['X-Download-Filename'] = quote(filename, safe='')
return resp
@router.api_route('/download/result', methods=['GET', 'POST'])
async def download_result(user: LoginUser, request: Request):
"""单曲下载结果:任务完成后流式返回文件。"""
data = await _request_data(request)
task_id = str(data.get('task_id') or '').strip()
if not task_id:
return error('缺少 task_id 参数')
with DOWNLOAD_TASKS_LOCK:
task = DOWNLOAD_TASKS.get(task_id)
if not task or task.get('type') != 'single':
return error('任务不存在或已过期', 404)
if not _owned_by(task, user):
return error('无权下载该任务', 403)
status = task.get('status')
if status in ('queued', 'running'):
return error('任务还在进行中,请稍后再试', 202)
if status == 'paused':
return error('任务已暂停,请重新打开页面后恢复', 200)
result = task.get('result') or {}
target = result.get('zip_path') or result.get('file_path')
target_exists = bool(target) and await asyncio.to_thread(os.path.exists, target)
if status in ('canceled', 'error') and not target_exists:
return error(task.get('msg') or ('任务已取消' if status == 'canceled' else '任务失败'),
200 if status == 'canceled' else 500)
if not target_exists:
return error('文件不存在或已被清理', 404)
filename = result.get('filename') or 'download'
assert target is not None
return _file_response(target, filename, result.get('mimetype') or 'application/octet-stream')
@router.api_route('/download/batch_zip/result', methods=['GET', 'POST'])
async def download_batch_zip_result(user: LoginUser, request: Request):
"""批量打包结果:任务完成后流式返回 ZIP。"""
data = await _request_data(request)
task_id = str(data.get('task_id') or '').strip()
if not task_id:
return error('缺少 task_id 参数')
with DOWNLOAD_TASKS_LOCK:
task = DOWNLOAD_TASKS.get(task_id)
if not task or task.get('type') != 'batch':
return error('任务不存在或已过期', 404)
if not _owned_by(task, user):
return error('无权下载该任务', 403)
status = task.get('status')
result = task.get('result') or {}
zip_path = result.get('zip_path')
has_deliverable = bool(zip_path) and await asyncio.to_thread(os.path.exists, zip_path)
if status in ('queued', 'running'):
return error('任务还在进行中,请稍后再试', 202)
if status == 'paused':
return error('任务已暂停,请重新打开页面后恢复', 200)
# 取消/失败但已有部分成果的任务,成果必须可领取
if status in ('canceled', 'error') and not has_deliverable:
return error(task.get('msg') or ('任务已取消' if status == 'canceled' else '任务失败'),
200 if status == 'canceled' else 500)
if not has_deliverable:
return error('打包文件不存在或已被清理', 404)
zip_name = result.get('filename') or '批量下载.zip'
assert zip_path is not None
return _file_response(zip_path, zip_name, 'application/zip')