# -*- coding: utf-8 -*- """封面字节缓存(同专辑只从网络拉一次)。 (自 music_downloader.py 拆出,行为与原实现一致。) """ from __future__ import annotations import threading from collections import deque from ._http import ( get_http_session, ) # 封面字节缓存:同一专辑封面只从网络拉取一次(ffmpeg 写元数据 与 ZIP 打包 共用) # 容量抽成常量:淘汰循环按它判定,不再读 deque.maxlen(该属性类型为 int|None, # 一旦有人把 deque 改成无界,len() >= None 会直接 TypeError) _COVER_CACHE_MAX = 128 _COVER_CACHE: dict[str, bytes] = {} _COVER_CACHE_ORDER: deque[str] = deque(maxlen=_COVER_CACHE_MAX) _COVER_CACHE_LOCK = threading.Lock() def fetch_cover_bytes(pic_url: str) -> bytes | None: """下载并缓存封面图片字节;失败返回 None(字节预算 10MB,拒绝超大盘点阵/伪装文件)""" if not pic_url: return None with _COVER_CACHE_LOCK: cached = _COVER_CACHE.get(pic_url) if cached: return cached try: resp = get_http_session().get(pic_url, timeout=15, stream=True) try: resp.raise_for_status() try: declared = int(resp.headers.get('Content-Length') or 0) except ValueError: declared = 0 # 垃圾 Content-Length 不炸,按未知长度走流式 if declared > 10 * 1024 * 1024: return None # bytearray 就地追加(原 bytes += 是 O(n²) 拷贝,10MB 封面最坏多拷上百 MB) data = bytearray() for chunk in resp.iter_content(chunk_size=64 * 1024): data += chunk if len(data) > 10 * 1024 * 1024: return None if not data: return None data = bytes(data) finally: # 所有路径统一关闭响应(原 Content-Length 异常路径漏关) resp.close() except Exception: return None with _COVER_CACHE_LOCK: if pic_url not in _COVER_CACHE: # 简单 FIFO 淘汰,防止缓存无界增长 while len(_COVER_CACHE_ORDER) >= _COVER_CACHE_MAX: evict = _COVER_CACHE_ORDER.popleft() _COVER_CACHE.pop(evict, None) _COVER_CACHE[pic_url] = data _COVER_CACHE_ORDER.append(pic_url) return data