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/music_downloader/_concurrency.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

151 lines
4.9 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 -*-
"""文件锁与计数信号量。
(自 music_downloader.py 拆出,行为与原实现一致。)
"""
from __future__ import annotations
import threading
from pathlib import Path
# per-file 写锁:保证同一目标文件的 .part 临时文件同时只有一个写入者,
# 防止并发下载同名歌曲(不同用户/任务)互相覆盖导致文件损坏
# 锁表带引用计数与回收——无人引用时从表中移除,防止长期运行无界增长
# 计数在注册表守卫锁内完成("取出引用"即计数 +1),消除"取引用后、acquire 前
# 被另一个线程回收、第三个线程拿到新锁对象"的互斥破坏窗口
_FILE_LOCKS: dict[str, RefCountedFileLock] = {}
_FILE_LOCKS_GUARD = threading.Lock()
class RefCountedFileLock:
"""带引用计数的文件锁:refs 表示"已持有或已登记即将持有"的数量,归零可回收"""
def __init__(self):
self._lock = threading.Lock()
self.refs = 0
def acquire(self) -> None:
self._lock.acquire()
def release(self) -> None:
self._lock.release()
def locked(self) -> bool:
return self._lock.locked()
class _FileLockHandle:
"""兼容 `with get_file_lock(path):` 旧用法的句柄:语义与 file_lock_scope 一致"""
def __init__(self, path: Path):
self._path = path
self._lock = None
self._key = ''
def __enter__(self) -> RefCountedFileLock:
key = str(self._path)
with _FILE_LOCKS_GUARD:
lock = _FILE_LOCKS.get(key)
if lock is None:
lock = RefCountedFileLock()
_FILE_LOCKS[key] = lock
lock.refs += 1
self._lock = lock
self._key = key
try:
lock.acquire()
except BaseException:
# acquire 被打断时回滚引用计数并摘除空表项,防锁表条目永久泄漏
with _FILE_LOCKS_GUARD:
lock.refs -= 1
if lock.refs <= 0 and not lock.locked():
_FILE_LOCKS.pop(key, None)
self._lock = None
self._key = ''
raise
return lock
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if self._lock is None:
return
self._lock.release()
with _FILE_LOCKS_GUARD:
self._lock.refs -= 1
if self._lock.refs <= 0 and not self._lock.locked():
_FILE_LOCKS.pop(self._key, None)
self._lock = None
def get_file_lock(path: Path) -> _FileLockHandle:
"""兼容入口:返回可用 with 语句的锁句柄(引用计数在守卫锁内登记,无回收竞态)"""
return _FileLockHandle(path)
class CountingSemaphore:
"""带活跃/等待计数的信号量(线程安全),用于实时展示排队情况
用法与 threading.BoundedSemaphore 一致(支持 with 语句),
额外维护 active(正在执行)/ waiting(排队等待)两个计数,
供前端实时展示排队人数与预计等待时间。
"""
def __init__(self, value: int = 1):
self.capacity = max(1, int(value))
self._semaphore = threading.BoundedSemaphore(self.capacity)
self._lock = threading.Lock()
self.active = 0
self.waiting = 0
def acquire(self, blocking: bool = True) -> bool:
if not blocking:
if not self._semaphore.acquire(blocking=False):
return False
with self._lock:
self.active += 1
return True
with self._lock:
self.waiting += 1
try:
self._semaphore.acquire()
except BaseException:
with self._lock:
self.waiting = max(0, self.waiting - 1)
raise
with self._lock:
self.waiting = max(0, self.waiting - 1)
self.active += 1
return True
def acquire_cancellable(self, cancel_ev, poll: float = 1.0) -> bool:
"""可取消地等待名额:等待期间 cancel_ev 置位立即返回 False,
不再无期限阻塞线程与退款。返回 True 表示已获得名额。"""
import time as _time
with self._lock:
self.waiting += 1
try:
while True:
if cancel_ev is not None and cancel_ev.is_set():
return False
if self._semaphore.acquire(blocking=False):
with self._lock:
self.active += 1
return True
_time.sleep(poll)
finally:
with self._lock:
self.waiting = max(0, self.waiting - 1)
def release(self) -> None:
self._semaphore.release()
with self._lock:
self.active = max(0, self.active - 1)
def __enter__(self) -> CountingSemaphore:
self.acquire()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.release()